Specification — ZCLHRINFOTYPEREADER
Specification — ZCL_HR_INFOTYPE_READER
Status: Draft v1.0
Related: ../CLAUDE.md, infotype_reading_pattern.md, exception_class_spec.md
1. Purpose
Provide a single reusable entry point for any ABAP program that needs to read multiple PA infotypes for one personnel number over a date range and obtain the data as a time-sliced consolidated table — one row per interval of unchanged field values across all requested infotypes, with each requested infotype's records nested per row as a sub-table.
The class replaces ad-hoc sequences of CL_HRPA_READ_INFOTYPE=>READ_INFOTYPE calls scattered across reports and BAdIs, centralizing:
- Primary vs Secondary infotype dispatch
READvsREAD_WIDEselection- Buffering across repeated reads (two-level cache)
- Authority handling (
MISSING_AUTHas warning,no_auth_checkopt-out) - Lock-status handling (
SPRPS) - Time-slicing of heterogeneous infotype validity intervals into a single aggregated timeline
- Structured, typed exception handling and structured message collection
The internal reading mechanics are documented separately in infotype_reading_pattern.md. This document focuses on the public API and the aggregation behavior.
2. Naming and location
| Object | Name |
|---|---|
| Class | ZCL_HR_INFOTYPE_READER |
| Exception class | ZCX_HR_INFOTYPE_READER |
3. Public types
Published as public types of the class so callers can import them without duplication.
" -- Request: which infotypes to read, with optional subtype filter and mandatory flag
TYPES:
BEGIN OF ty_infotype_req,
infty TYPE infty, " e.g. '0001'
subty TYPE subty, " SPACE = all subtypes
mandatory TYPE abap_bool, " 'X' = empty result is a hard error
END OF ty_infotype_req,
ty_infotype_req_t TYPE STANDARD TABLE OF ty_infotype_req WITH DEFAULT KEY.
" -- Returned message (for caller-side logging; NOT raised)
TYPES:
BEGIN OF ty_message,
msgty TYPE symsgty, " 'S' / 'I' / 'W' / 'E' — typically 'W'
msgid TYPE symsgid,
msgno TYPE symsgno,
msgv1 TYPE symsgv,
msgv2 TYPE symsgv,
msgv3 TYPE symsgv,
msgv4 TYPE symsgv,
text TYPE string, " pre-formatted human-readable text (convenience)
pernr TYPE persno, " context
infty TYPE infty, " context (SPACE if not infotype-specific)
END OF ty_message,
ty_message_t TYPE STANDARD TABLE OF ty_message WITH DEFAULT KEY.
" -- Execution statistics (diagnostic output)
TYPES:
BEGIN OF ty_stats,
infotypes_requested TYPE i,
infotypes_read TYPE i, " actually hit DB (excludes buffer hits)
records_read TYPE i, " total PRELP rows fetched from DB
buffer_hits TYPE i,
buffer_misses TYPE i,
intervals_generated TYPE i, " before collapse
intervals_final TYPE i, " after collapse
runtime_ms TYPE i,
END OF ty_stats.
4. Public constants
CONSTANTS:
" Lock-status filter (maps onto IF_HRPA_READ_INFOTYPE SPRPS values)
BEGIN OF c_sprps,
unlocked TYPE char1 VALUE '0', " active records only (default)
locked TYPE char1 VALUE '1', " locked records only
all TYPE char1 VALUE '2', " both
END OF c_sprps,
" Read mode
BEGIN OF c_read_mode,
buffer_first TYPE char1 VALUE 'B', " default: cache, fall back to DB
database TYPE char1 VALUE 'D', " bypass cache, always hit DB (still populates cache)
buffer_only TYPE char1 VALUE 'M', " cache only, never hit DB; miss -> empty
END OF c_read_mode.
Note that c_sprps values mirror the framework's IF_HRPA_READ_INFOTYPE=>UNLOCKED / LOCKED / ALL constants. Exposing them as a local enum avoids cluttering every caller with IF_HRPA_* references.
5. Factory
The factory is the only way to obtain an instance — the class is CREATE PRIVATE, so callers must go through create( … ).
CLASS-METHODS create
IMPORTING
iv_enable_buffer TYPE abap_bool DEFAULT abap_true
iv_no_auth_check TYPE abap_bool DEFAULT abap_false
iv_sprps TYPE char1 DEFAULT c_sprps-unlocked
iv_read_mode TYPE char1 DEFAULT c_read_mode-buffer_first
iv_collapse_equal TYPE abap_bool DEFAULT abap_true
RETURNING
VALUE(ro_reader) TYPE REF TO zcl_hr_infotype_reader.
| Parameter | Purpose |
|---|---|
iv_enable_buffer | If abap_false, every call re-reads from DB. Class-level meta cache still used. |
iv_no_auth_check | Pass-through to framework no_auth_check. Only set from contexts where auth is pre-established. |
iv_sprps | Lock-status filter. Default unlocked — exclude locked records. |
iv_read_mode | See constants in §4. |
iv_collapse_equal | If abap_true (default), adjacent time-sliced rows with byte-equal nested tables merge into one row with widened BEGDA..ENDDA. Turn off for debugging slice logic. |
The factory never raises — all work that can fail is deferred to the read methods.
6. Main methods
6.1 Typed mode (recommended)
The caller pre-declares a structured line type whose components correspond to the infotypes they intend to read, passes an empty internal table via CHANGING, and the method fills it.
METHODS read_aggregated_typed
IMPORTING
iv_pernr TYPE persno
iv_begda TYPE begda
iv_endda TYPE endda
it_infotypes TYPE ty_infotype_req_t
EXPORTING
et_messages TYPE ty_message_t
es_stats TYPE ty_stats
CHANGING
ct_result TYPE STANDARD TABLE " caller-declared structured table
RAISING
zcx_hr_infotype_reader.
The line type of CT_RESULT must have at least these components:
| Component | Type | Required |
|---|---|---|
PERNR | PERSNO (compatible) | yes |
BEGDA | BEGDA | yes |
ENDDA | ENDDA | yes |
T_IT<NNNN> | STANDARD TABLE OF P<NNNN> or structurally compatible | one per entry in IT_INFOTYPES |
Example caller line type:
TYPES:
BEGIN OF ty_out_data,
pernr TYPE persno,
begda TYPE begda,
endda TYPE endda,
t_it0000 TYPE STANDARD TABLE OF p0000 WITH DEFAULT KEY,
t_it0001 TYPE STANDARD TABLE OF p0001 WITH DEFAULT KEY,
t_it0002 TYPE STANDARD TABLE OF p0002 WITH DEFAULT KEY,
END OF ty_out_data,
ty_out_data_t TYPE STANDARD TABLE OF ty_out_data WITH DEFAULT KEY.
6.2 Dynamic mode (alternative)
The class builds the line type on the fly using RTTC and returns a data reference to a dynamically-typed structured table.
METHODS read_aggregated_dynamic
IMPORTING
iv_pernr TYPE persno
iv_begda TYPE begda
iv_endda TYPE endda
it_infotypes TYPE ty_infotype_req_t
EXPORTING
er_result TYPE REF TO data " dynamically built structured table
ero_result_type TYPE REF TO cl_abap_tabledescr " RTTI handle for caller introspection
et_messages TYPE ty_message_t
es_stats TYPE ty_stats
RAISING
zcx_hr_infotype_reader.
The built line type has components named exactly PERNR, BEGDA, ENDDA, T_IT<NNNN> where <NNNN> matches every distinct INFTY in IT_INFOTYPES (de-duplicated, sorted ascending). Each nested table is typed STANDARD TABLE OF P<NNNN>.
Callers access the result via field symbols:
DATA(lo_reader) = zcl_hr_infotype_reader=>create( ).
DATA lr_result TYPE REF TO data.
DATA lro_result_type TYPE REF TO cl_abap_tabledescr.
lo_reader->read_aggregated_dynamic(
EXPORTING
iv_pernr = '00000001'
iv_begda = '20260101'
iv_endda = '20261231'
it_infotypes = VALUE #( ( infty = '0001' ) ( infty = '0002' ) )
IMPORTING
er_result = lr_result
ero_result_type = lro_result_type
).
FIELD-SYMBOLS: <lt_result> TYPE STANDARD TABLE.
ASSIGN lr_result->* TO <lt_result>.
LOOP AT <lt_result> ASSIGNING FIELD-SYMBOL(<ls_row>).
ASSIGN COMPONENT 'T_IT0001' OF STRUCTURE <ls_row> TO FIELD-SYMBOL(<lt_it0001>).
" ...
ENDLOOP.
6.3 When to use which mode — Design Decision
Design decision — typed mode is the default recommendation.
Typed mode gives compile-time field access, IDE completion, and the clearest call site, matching the pattern in your original example. Dynamic mode exists for the case where the infotype list is assembled at runtime (e.g. from a Customizing table or a selection screen), and declaring a matching static type is impractical.
The typed-mode validator in the class (see §8) enforces structural compatibility with a clear error if the caller's type drifts from the requested
IT_INFOTYPES, so the two modes are interchangeable in semantics, only differing in who owns the line type.
6.4 Auto-expansion of linked infotypes
Before reading and before typed-mode validation, the class internally expands IT_INFOTYPES with the linked partner (primary ↔ secondary) of every requested infotype that has one, on the resolved MOLGA. The original entries are preserved (order, subtype filter, mandatory flag); the appended partner entries carry mandatory = abap_false and no subtype filter.
Consequences:
- If the caller requests
0001andT582V/T582Wlink it to0003on the employee's MOLGA, the effective request becomes{ 0001, 0003 },READ_WIDEfires once, and the final result contains bothT_IT0001andT_IT0003. - If the caller requests only the secondary (say
0003), the effective request becomes{ 0003, 0001 },READ_WIDEreads the primary, and the result containsT_IT0003andT_IT0001. - Typed-mode callers therefore need
T_IT<NNNN>components for any linked partner of the infotypes they request, or they will getSTRUCTURE_MISMATCH. When in doubt, declare both sides of a known pair (0001/0003,0008/0052, etc.) in the caller's line type. - Dynamic mode builds the line type from the expanded list automatically, so the caller receives
T_IT<NNNN>components for the partners as well.
7. Buffer helpers
METHODS clear_buffer. " drop all instance-level PRELP cache entries
METHODS clear_buffer_for_pernr IMPORTING iv_pernr TYPE persno.
METHODS get_buffer_stats RETURNING VALUE(rs_stats) TYPE ty_stats.
The class-level meta cache (GT_META_CACHE) is not cleared by these methods — it is static per MOLGA and its content is never invalidated within a program run. A public class method CLEAR_META_CACHE is exposed for unit testing; production callers should not invoke it.
8. RTTI validation (typed mode)
At the start of READ_AGGREGATED_TYPED, after IT_INFOTYPES has been expanded with linked partners (§6.4), validate that the caller's CT_RESULT line type is compatible with the expanded infotype list. Validation is strict on missing components and lenient on extras:
- Describe the line type via
CL_ABAP_STRUCTDESCR=>DESCRIBE_BY_DATAon a dummy line. - Verify these mandatory components exist:
PERNR,BEGDA,ENDDA(existence check by name). - For every
INFTYin the expanded request (deduplicated), verify a component namedT_IT<NNNN>exists and haskind = KIND_TABLE. Structural compatibility of the nested line type is not checked up-front — actual compatibility withP<NNNN>is enforced at serve time byCL_HR_PNNNN_TYPE_CAST=>PRELP_TO_PNNNN_TAB, which will raiseCX_HRPA_VIOLATED_ASSERTIONif incompatible. That assertion is re-raised asCONVERSION_FAILURE(§10). - Extra components in the line type (neither required meta components nor any
T_IT<NNNN>for a requested or auto-expanded infotype) trigger a warning inET_MESSAGESbut do not fail the call. - Any missing mandatory component or any missing
T_IT<NNNN>(for either a requested or an auto-expanded infotype) raisesZCX_HR_INFOTYPE_READERwithtextid = STRUCTURE_MISMATCHandexc_no = 4. No read is performed.
9. Aggregation algorithm — time-slicing
After all requested infotypes are read (via the pattern in infotype_reading_pattern.md), the class builds the output table as follows.
9.1 Input to the aggregator
V_BEGDA,V_ENDDA— the caller's window.- For each infotype
I_kinIT_INFOTYPES: a list of recordsR_{k,1} .. R_{k,n_k}, each withBEGDA,ENDDAand payload fields. Records are those returned by the framework as overlapping the window (see pattern doc §5), i.e. every record withR.BEGDA ≤ V_ENDDAANDR.ENDDA ≥ V_BEGDA.
9.2 Step 1 — collect boundary dates
Initialize boundaries = { V_BEGDA, V_ENDDA + 1 }.
For every record of every requested infotype:
- If
R.BEGDA > V_BEGDA, addR.BEGDAto boundaries. - If
R.ENDDA < V_ENDDA, addR.ENDDA + 1to boundaries.
Dates clipped below V_BEGDA or above V_ENDDA + 1 are discarded (they would fall outside the window).
Sort and deduplicate — this yields B_1 < B_2 < ... < B_m with B_1 = V_BEGDA and B_m = V_ENDDA + 1.
9.3 Step 2 — form intervals
Intervals are [B_j, B_{j+1} - 1] for j = 1 .. m - 1. Each interval is non-empty and non-overlapping with the others, and their union is exactly [V_BEGDA, V_ENDDA].
If no records exist for any infotype, the only boundaries are { V_BEGDA, V_ENDDA + 1 } and a single interval [V_BEGDA, V_ENDDA] is produced with all nested tables empty.
9.4 Step 3 — snapshot each interval
For each interval [I_BEG, I_END] and each requested infotype I_k:
- Collect every record of
I_kthat is active in the interval:R.BEGDA ≤ I_END AND R.ENDDA ≥ I_BEG. - Place those records, as-is, into the interval's nested table
T_IT<NNNN>.
An output row is then constructed:
| Field | Value |
|---|---|
PERNR | IV_PERNR |
BEGDA | I_BEG |
ENDDA | I_END |
T_IT<NNNN> | records of infotype <NNNN> active in [I_BEG, I_END] |
Design decision — records in nested tables retain their original
BEGDA/ENDDA.Your requirement text says nested tables should be "without BEGDA/ENDDA" because those are lifted to the outer row. Your code example, however, uses
STANDARD TABLE OF P0001which structurally containsBEGDA/ENDDA. The simplest design that honors both is:
- The outer row's
BEGDA/ENDDAis the canonical interval for that snapshot.- The nested records'
BEGDA/ENDDAhold the original record's validity, preserving traceability to the source row inPA<NNNN>.This costs nothing in memory or correctness and is a common debugging lifesaver. If a caller genuinely wants stripped-date nested tables, they can declare their own flat structure (e.g.
TY_P0001_FLATwithoutBEGDA/ENDDA/SEQNR) as the nested line type, andMOVE-CORRESPONDINGwill do the right thing.If you want the stricter "nested records must have no BEGDA/ENDDA" contract instead, flip a factory flag
iv_strip_nested_dates. I haven't added it to the signature above because the default is cleaner without it — raise it in review and I'll add.
9.5 Step 4 — collapse equal adjacent rows (optional)
If IV_COLLAPSE_EQUAL = abap_true (factory default):
Iterate the output in ascending BEGDA order. For each pair of consecutive rows R_j, R_{j+1} where R_j.ENDDA + 1 = R_{j+1}.BEGDA and every T_IT<NNNN> table in R_j is byte-equal to the corresponding table in R_{j+1} (comparison by serialized content, not by reference): merge them — set R_j.ENDDA = R_{j+1}.ENDDA and delete R_{j+1}.
Repeat until no more merges are possible. In practice one pass suffices because once a merge happens the merged row's content has not changed, so further merge candidates are tested against the merged row.
Design decision — "equal" is content-equal, not source-identical.
Two snapshots are considered equal when every payload field in every record of every nested table matches. This includes record-level
BEGDA/ENDDAif they are part of the nested structure — which means that when the nested rows' own validity fields differ between two otherwise-equal intervals (e.g. because the source record switched but kept all payload fields), the rows will NOT collapse. This is usually the right behavior for audit but can be surprising. Callers who want collapse on payload-only should declare a stripped nested type (see §9.4 design note).
9.6 Example
Employee 00000001, window 01.01.2026 .. 31.12.2026, requested [0001, 0002].
PA0001data:[01.01.2026 .. 30.06.2026]WERKS='PL01' ORGEH='10000001'[01.07.2026 .. 31.12.2026]WERKS='PL01' ORGEH='10000002'
PA0002data:[01.01.2026 .. 31.03.2026]NACHN='Kowalski' ANRED='Mr'[01.04.2026 .. 31.12.2026]NACHN='Nowak' ANRED='Mr'
Boundaries: { 01.01.2026, 01.04.2026, 01.07.2026, 01.01.2027 }
Intervals: [01.01..31.03], [01.04..30.06], [01.07..31.12]
BEGDA | ENDDA | T_IT0001 (payload) | T_IT0002 (payload) |
|---|---|---|---|
| 01.01.2026 | 31.03.2026 | ORGEH=10000001 | NACHN=Kowalski |
| 01.04.2026 | 30.06.2026 | ORGEH=10000001 | NACHN=Nowak |
| 01.07.2026 | 31.12.2026 | ORGEH=10000002 | NACHN=Nowak |
No adjacent intervals collapse (every pair differs in at least one nested table).
10. Warnings vs exceptions
Warnings are always appended to ET_MESSAGES with MSGTY = 'W' and never raise. Exceptions always raise ZCX_HR_INFOTYPE_READER with a defined textid and exc_no. The complete classification is in exception_class_spec.md §6.
The key rule: anything per-infotype is a warning (skip that infotype, continue). Anything per-call is an exception (abort the whole call). MANDATORY_INFOTYPE_EMPTY is the only exception to this rule — it promotes a per-infotype condition to a hard error when the caller flagged the infotype as mandatory.
11. Example — typed mode
REPORT zhr_test_reader.
TYPES:
BEGIN OF ty_out_data,
pernr TYPE persno,
begda TYPE begda,
endda TYPE endda,
t_it0000 TYPE STANDARD TABLE OF p0000 WITH DEFAULT KEY,
t_it0001 TYPE STANDARD TABLE OF p0001 WITH DEFAULT KEY,
t_it0002 TYPE STANDARD TABLE OF p0002 WITH DEFAULT KEY,
END OF ty_out_data,
ty_out_data_t TYPE STANDARD TABLE OF ty_out_data WITH DEFAULT KEY.
DATA lt_result TYPE ty_out_data_t.
DATA lt_msg TYPE zcl_hr_infotype_reader=>ty_message_t.
DATA ls_stats TYPE zcl_hr_infotype_reader=>ty_stats.
TRY.
DATA(lo_reader) = zcl_hr_infotype_reader=>create( ).
lo_reader->read_aggregated_typed(
EXPORTING
iv_pernr = '00000001'
iv_begda = '20260101'
iv_endda = '20261231'
it_infotypes = VALUE #(
( infty = '0000' mandatory = abap_true )
( infty = '0001' mandatory = abap_true )
( infty = '0002' )
)
IMPORTING
et_messages = lt_msg
es_stats = ls_stats
CHANGING
ct_result = lt_result
).
" consume lt_result, log lt_msg, report ls_stats
CATCH zcx_hr_infotype_reader INTO DATA(lx).
MESSAGE lx TYPE 'E'.
ENDTRY.
12. Test plan (minimum coverage)
| # | Scenario | Expected |
|---|---|---|
| 1 | Single infotype, single unchanged record | One output row, window-wide |
| 2 | Single infotype, two adjacent records with different payloads | Two rows, no collapse |
| 3 | Two infotypes with overlapping but non-aligned splits | Three rows (per §9.6 example) |
| 4 | Primary with linked secondary (e.g. 0001/0003 on MOLGA 46) | READ_WIDE called once; both cached |
| 5 | Secondary requested alone | Owning primary resolved; READ_WIDE on primary; secondary served from cache |
| 6 | Invalid PERNR (no PA0001 overlap) | ZCX_HR_INFOTYPE_READER with exc_no = 1 |
| 7 | BEGDA > ENDDA | ZCX_HR_INFOTYPE_READER with exc_no = 2 |
| 8 | Empty IT_INFOTYPES | ZCX_HR_INFOTYPE_READER with exc_no = 3 |
| 9 | Caller's structure missing a required T_IT<NNNN> | ZCX_HR_INFOTYPE_READER with exc_no = 4 |
| 10 | MISSING_AUTH on one infotype | Warning in ET_MESSAGES, nested table empty, other infotypes populated |
| 11 | Mandatory infotype returns empty | ZCX_HR_INFOTYPE_READER with exc_no = 7 |
| 12 | Second call with same PERNR+infotype | Buffer hit, no DB read, ES_STATS-BUFFER_HITS = 1 |
| 13 | IV_COLLAPSE_EQUAL = false vs true | Same data, different row counts when flat segments exist |
| 14 | SPRPS = LOCKED | Only locked records present in output |
| 15 | Subtype filter on 0041 (Date Specifications) | Only rows with matching SUBTY in T_IT0041 |
| 16 | Customer infotype with blank T777D-INFKN | Warning, falls back to READ as primary |
| 17 | Record with BEGDA = '00010101' / ENDDA = '99991231' | Clipped intervals use V_BEGDA and V_ENDDA |
13. What is explicitly NOT in this class
See CLAUDE.md §"Out of scope". Highlights: no writes, no cluster reads, no OM infotypes, no payroll results, no multi-PERNR, no time evaluation.
14. Open questions (flagged for review)
- Nested date stripping — §9.4 leaves
BEGDA/ENDDAin the nested records. Should the factory exposeiv_strip_nested_datesas a knob? Default answer: no, callers who need it declare a flat nested type. - MOLGA change within window — §7 of the pattern doc uses MOLGA from the
BEGDA-validPA0001. If the employee has a country transfer during the window, the metadata resolution is pinned. Acceptable? Alternative: re-resolve per interval after the first pass. VINFT-based linkage cardinality — the current resolver assumes at most one linked secondary per primary (SELECT SINGLE). If a view has multiple secondaries, only the first (by DB read order) wins. Confirm this is acceptable for our infotype set, or change toSELECTwith collection.- Auto-expansion opt-out — §6.4 unconditionally appends linked partners to
IT_INFOTYPES. A caller who wants only the requested side (e.g. requests0001, does not wantT_IT0003in the result for MOLGA 46) currently has no way to opt out. Default answer: accept the widening because the physical read already fetches both viaREAD_WIDE; revisit if a concrete caller objects. - Warning message numbers — the implementation appends every runtime warning (missing_auth, orphan secondary, unknown
INFKN, extra structure components) withmsgno = '010', which collides with theREADER_NOT_AVAILABLEtext in message classZHR_INFOTYPE_READER. Allocate dedicated warning message numbers (proposedW01..W09) and updateappend_warningaccordingly.