Infotype Reading Pattern (IF_HRPA_READ_INFOTYPE + Primary/Secondary + Buffering)

Status: Draft v1.0 Audience: Implementers of ZCL_HR_INFOTYPE_READER Related: infotype_reader_spec.md, exception_class_spec.md

This document describes the internal reading pattern that ZCL_HR_INFOTYPE_READER uses to fetch raw infotype data before aggregation. It is derived from the production pattern captured in read_infotype_example.txt and adapted for range reads (BEGDA..ENDDA) rather than single-key-date reads.

The pattern is deliberately separated from the main class spec so the same approach can be reused by other HR reader utilities without duplicating the explanation.


1. Why CL_HRPA_READ_INFOTYPE and not HR_READ_INFOTYPE

HR_READ_INFOTYPE is a classic function module that pre-dates decoupled infotypes. On S/4HANA with the decoupled infotype framework enabled (this system has it enabled), the FM still works for simple primary infotypes but is in maintenance mode and fails silently or with cryptic errors for certain classes of infotypes — in particular those that have switched to a decoupled storage model.

CL_HRPA_READ_INFOTYPE is the current framework entry point. It returns an instance of the interface IF_HRPA_READ_INFOTYPE, which exposes:

  • READ — read one infotype into a PRELP_TAB for one employee, one infotype, optionally with SUBTY filter, over a date range
  • READ_WIDE — read a primary infotype together with its linked secondary in a single call, returning pairs of rows (primary + secondary) in HRPAD_INFOTYPE_DATA_TAB

Both methods accept BEGDA/ENDDA as a range — this is used directly (the user requirement is a range, not a key date).

Obtain the instance once per reader:

DATA mo_infty_reader TYPE REF TO if_hrpa_read_infotype.

TRY.
    cl_hrpa_read_infotype=>get_instance(
      IMPORTING infotype_reader = mo_infty_reader ).
  CATCH cx_hrpa_violated_assertion INTO DATA(lx_root).
    RAISE EXCEPTION TYPE zcx_hr_infotype_reader
      EXPORTING
        textid    = zcx_hr_infotype_reader=>reader_not_available
        exc_no    = zcx_hr_infotype_reader=>c_exc_no-reader_not_available
        previous  = lx_root.
ENDTRY.

Failure to obtain the instance is a hard error — the reader is unusable. Every other error mode is localized per infotype.


2. Primary vs Secondary infotypes

T777D-INFKN classifies infotypes (globally, independent of MOLGA):

INFKNMeaningExampleHow it's read
'I'Primary / individual0001, 0002READ — unless it has a linked secondary (see below)
'Z'Secondary0003Not read directly — read via its owning primary with READ_WIDE
(blank or other)Unknowncustomer infotypesFallback to READ as primary

Linkage between a primary and its secondary is MOLGA-dependent and is resolved via two tables:

  • T582V: primary infotype → view identifier (VINFT). Keyed by (MOLGA, INFTY).
  • T582W: view identifier → list of infotypes assigned to the view. Keyed by (VINFT, INFTY).

The same VINFT appears in both tables; T582V lists only the primary, T582W lists all infotypes (including the primary itself) that belong to the view. The linked secondary is the row in T582W with the same VINFT but a different INFTY than the primary — provided that candidate's own T777D-INFKN is 'Z'.

Linkage resolution algorithm

Given infotype X and personnel MOLGA M:

  1. Look up T777D-INFKN for X. Call it INFKN_X.
  2. If INFKN_X = 'I':
    • SELECT SINGLE vinft FROM t582v WHERE infty = X AND molga = M. If nothing found, X has no linked secondary — it is a standalone primary.
    • If vinft found: SELECT SINGLE infty FROM t582w WHERE vinft = vinft AND infty <> X.
      • If nothing found, still standalone primary.
      • If a candidate Y is found, verify T777D-INFKN for Y equals 'Z'. If yes, Y is the linked secondary. If not, discard and treat X as standalone primary.
  3. If INFKN_X = 'Z':
    • SELECT SINGLE vinft FROM t582w WHERE infty = X. If nothing found, X is an orphan secondary — log warning, skip.
    • If vinft found: SELECT SINGLE infty FROM t582v WHERE vinft = vinft AND molga = M AND infty <> X.
      • If nothing found, orphan secondary — log warning, skip.
      • If a candidate P is found, verify T777D-INFKN for P equals 'I'. If yes, P is the owning primary. If not, discard and treat X as orphan.
  4. If INFKN_X is blank or another value, treat X as a standalone primary ('I' fallback).

This resolution must be cached at class level keyed by (INFTY, MOLGA). For one program run the metadata is static; re-running the three SELECT SINGLEs for every employee would be wasted I/O.


3. Read paths

3.1 Standalone primary — READ

Primary infotype with no linked secondary (either T582V has no entry for the given (MOLGA, INFTY), or the resolution rejected the candidate):

DATA lt_prelp        TYPE prelp_tab.
DATA lv_missing_auth TYPE boolean.

TRY.
    mo_infty_reader->read(
      EXPORTING
        tclas         = cl_hrpa_tclas=>tclas_employee
        pernr         = iv_pernr
        infty         = iv_infty
        subty         = iv_subty               " SPACE = no subtype filter
        begda         = iv_begda
        endda         = iv_endda
        sprps         = if_hrpa_read_infotype=>unlocked
        no_auth_check = iv_no_auth_check
      IMPORTING
        infotype_tab  = lt_prelp
        missing_auth  = lv_missing_auth ).
  CATCH cx_hrpa_violated_assertion INTO DATA(lx).
    RAISE EXCEPTION TYPE zcx_hr_infotype_reader
      EXPORTING
        textid   = zcx_hr_infotype_reader=>read_failure
        exc_no   = zcx_hr_infotype_reader=>c_exc_no-read_failure
        pernr    = iv_pernr
        infty    = iv_infty
        previous = lx.
ENDTRY.

SPRPS is the lock indicator of the returned records. IF_HRPA_READ_INFOTYPE only exposes UNLOCKED and LOCKED as named constants; there is no ALL constant. The class's c_sprps values map as follows (see SPRPS_TO_FRAMEWORK in the implementation):

c_sprps valueFramework value passed to READ / READ_WIDE
unlocked ('0')IF_HRPA_READ_INFOTYPE=>UNLOCKED
locked ('1')IF_HRPA_READ_INFOTYPE=>LOCKED
all ('2')literal '*' — wildcard meaning "all lock statuses"

The class exposes this as an input parameter so callers that need full auditability (e.g. migration validation) can request all statuses. Default is UNLOCKED.

3.2 Primary with linked secondary — READ_WIDE

Returns rows of HRPAD_INFOTYPE_DATA_TAB where each row contains a PRIMARY_INFTY and a SECONDARY_INFTY field (both of type PRELP). One call yields both infotypes' data:

DATA lt_wide          TYPE hrpad_infotype_data_tab.
DATA lv_missing_auth  TYPE boolean.

TRY.
    mo_infty_reader->read_wide(
      EXPORTING
        tclas         = cl_hrpa_tclas=>tclas_employee
        pernr         = iv_pernr
        infty         = iv_primary_infty       " the PRIMARY, even if caller asked for the secondary
        sprps         = if_hrpa_read_infotype=>unlocked
        begda         = iv_begda
        endda         = iv_endda
        no_auth_check = iv_no_auth_check
      IMPORTING
        infotype_tab  = lt_wide
        missing_auth  = lv_missing_auth ).
  CATCH cx_hrpa_violated_assertion INTO DATA(lx).
    RAISE EXCEPTION TYPE zcx_hr_infotype_reader
      EXPORTING
        textid   = zcx_hr_infotype_reader=>read_failure
        exc_no   = zcx_hr_infotype_reader=>c_exc_no-read_failure
        pernr    = iv_pernr
        infty    = iv_primary_infty
        previous = lx.
ENDTRY.

After the wide read, split lt_wide into two PRELP tables — one for the primary, one for the secondary — and insert both into the instance PRELP cache. The secondary entry is inserted even if the caller originally asked only for the primary, because a subsequent request for the secondary (within the same instance lifetime for the same employee) will then be served from the cache at no extra I/O.

Each row of HRPAD_INFOTYPE_DATA_TAB carries two nested PRELP records as the components PRIMARY_INFTY and SECONDARY_INFTY. The implementation splits the table by appending rows whose nested PRIMARY_INFTY-INFTY (resp. SECONDARY_INFTY-INFTY) is non-initial:

LOOP AT lt_wide INTO DATA(ls_wide).
  IF ls_wide-primary_infty-infty   IS NOT INITIAL. APPEND ls_wide-primary_infty   TO lt_primary_prelp.   ENDIF.
  IF ls_wide-secondary_infty-infty IS NOT INITIAL. APPEND ls_wide-secondary_infty TO lt_secondary_prelp. ENDIF.
ENDLOOP.

Both tables are then cached via cache_put( pernr, infty, t_prelp ) — the secondary's infotype is taken from the linkage metadata, not from the record itself, so the cache key is consistent even when the framework returns an empty secondary side.

3.3 Secondary requested on its own

The only correct path is:

  1. Resolve the owning primary P via T582W → T582V.
  2. READ_WIDE the primary — this populates cache entries for both P and the secondary.
  3. Read the secondary from the cache.

ZCL_HR_INFOTYPE_READER must not attempt to read a secondary directly with READ — this produces either empty results or framework errors depending on the infotype.

3.4 Auto-expansion of linked infotypes

The top-level READ_AGGREGATED_* methods call EXPAND_REQUESTED_INFOTYPES before any read. This routine walks the caller's IT_INFOTYPES, resolves each entry's metadata, and appends the linked partner (primary ↔ secondary) to the effective request list when it is not already present. The partner is appended with mandatory = abap_false and no subtype filter.

Effect on the read flow:

  • The READ_WIDE call is still issued exactly once per primary, because the per-infotype dispatcher sees the partner as already cached after the first wide read.
  • Both the requested infotype and the partner appear in the caller's result structure (see reader spec §6.4). Typed-mode callers must declare T_IT<NNNN> components for the partners as well, or validation will raise STRUCTURE_MISMATCH.

This is a behavior of the aggregator, not of the framework — READ_WIDE always returns both sides; the aggregator now also surfaces both sides.


4. PRELP cache and PRELP → PNNNN conversion

Why PRELP is cached, not PNNNN

PRELP is the physical transport format used by the infotype framework. Converting to P<NNNN> (e.g. P0001, P0002) is done per call, not at cache-insert time, because:

  • The target type in the caller's output differs per call (the caller may pass P0001 in one call and a custom flat structure in another).
  • PRELP is a single generic type; caching it keeps the cache flat and type-agnostic.
  • Conversion overhead is negligible compared to DB I/O.

Conversion

cl_hr_pnnnn_type_cast=>prelp_to_pnnnn_tab(
  EXPORTING prelp_tab = lt_prelp_from_cache
  IMPORTING pnnnn_tab = ct_typed_tab ).      " STANDARD TABLE OF P<NNNN>

CT_TYPED_TAB is typed generically as STANDARD TABLE at the method signature; the caller-supplied type is respected by the cast. If the target table's line type doesn't match P<NNNN> structurally, PRELP_TO_PNNNN_TAB will raise an assertion — catch it and map to STRUCTURE_MISMATCH.

Cache key and lifecycle

CacheScopeKeyLifetime
GT_META_CACHEClass-level(INFTY, MOLGA)Program run (shared across instances)
MT_PRELP_CACHEInstance(PERNR, INFTY)Reader instance lifetime

The class-level meta cache is never cleared within a program run — T777D/T582V/T582W data is effectively static per MOLGA. The instance-level PRELP cache is cleared by CLEAR_BUFFER (all) or CLEAR_BUFFER_FOR_PERNR (one employee).

Empty-result caching

An empty PRELP result (lt_prelp IS INITIAL) is still cached as an empty entry. This distinguishes "read happened, nothing found" from "not yet read". The same applies to the missing_auth case — insert an empty entry for the infotype so the same employee is not re-tried within the instance lifetime.


5. Range reads — what "active within BEGDA..ENDDA" means

READ and READ_WIDE with a BEGDA..ENDDA range return every infotype record whose validity interval overlaps the requested window at least partially:

  • Record with validity [R_BEG, R_END] is returned when R_BEG ≤ V_END AND R_END ≥ V_BEG.

The framework does not clip the returned record's own dates to the requested window — callers receive the record as stored. The aggregation step in ZCL_HR_INFOTYPE_READER is responsible for any window clipping and for the time-slicing described in the main class spec.

BEGDA/ENDDA sentinels

Two common sentinels appear in HR data:

  • BEGDA = '00010101' (earliest possible date) — records valid "from the beginning of time"
  • ENDDA = '99991231' (latest possible date) — records valid "to the end of time"

The reader must handle these without special-casing — they are simply large/small values in date arithmetic.


6. Authorization — MISSING_AUTH is a warning, not an exception

Both READ and READ_WIDE return a MISSING_AUTH TYPE BOOLEAN flag. When set, the caller (employee) has no read authorization for the infotype, no data is returned, and the operation is not an error in the ABAP exception sense.

Policy for ZCL_HR_INFOTYPE_READER:

  1. Append a warning to ET_MESSAGES (MSGTY = 'W') with the (PERNR, INFTY) context.
  2. Insert an empty cache entry so the same infotype is not retried for the same employee.
  3. Do not raise. Continue with the next infotype.
  4. In the final output, the nested table for this infotype is empty.

Rationale: batch runs, especially reconciliation and data migration jobs, routinely encounter per-employee authorization gaps that the ops team expects to be reported, not aborted.

Opting out of authority checks

For background utility contexts where an overarching authorization concept has already been established (e.g. a report that already executed a PNP auth check, or a batch job under a service user with a documented blanket authorization), the factory offers iv_no_auth_check = abap_true. This sets the no_auth_check parameter on every framework call. Use with discipline — it should be an explicit, audited decision per caller, not a convenience default.


7. Resolving MOLGA

Linkage resolution needs the employee's country grouping. Determine it once per READ_AGGREGATED_* call from PA0001 valid at the input BEGDA (or ENDDA — either works for this purpose since MOLGA does not change for normal employees):

SELECT SINGLE molga FROM pa0001 INTO @mv_molga
  WHERE pernr = @iv_pernr
    AND begda <= @iv_begda
    AND endda >= @iv_begda.
IF sy-subrc <> 0.
  " Fall back to ENDDA or to the period that contains any date in the window
  SELECT SINGLE molga FROM pa0001 INTO @mv_molga
    WHERE pernr = @iv_pernr
      AND begda <= @iv_endda
      AND endda >= @iv_begda.
ENDIF.
IF sy-subrc <> 0.
  RAISE EXCEPTION TYPE zcx_hr_infotype_reader
    EXPORTING
      textid = zcx_hr_infotype_reader=>invalid_pernr
      exc_no = zcx_hr_infotype_reader=>c_exc_no-invalid_pernr
      pernr  = iv_pernr.
ENDIF.

If the employee has no PA0001 record overlapping the window, the PERNR is treated as invalid for this call — raise INVALID_PERNR. This is the one case where absence of data is a hard error rather than an empty result.

Store MV_MOLGA as an instance attribute; it is stable for the call.


8. Subtype filter

A SUBTY filter is per-infotype, not per-call. The main class's input table IT_INFOTYPES (type TY_INFOTYPE_REQ_T) carries an optional SUBTY per requested infotype. When reading, pass it to READ:

  • If SUBTY = SPACE — no subtype filter, read all subtypes.
  • Otherwise pass it as-is.

Caveat for READ_WIDE: the SAP API does not expose a SUBTY parameter on READ_WIDE. When reading a primary with linked secondary, all subtypes of both infotypes are fetched into the cache. The subtype filter is then applied at serve time — when converting the cached PRELP to typed rows for the caller. Filter by SUBTY field on the typed P<NNNN> rows after the cast.

Document this as a behavioral note on the IT_INFOTYPES parameter in the main spec.


9. Exception mapping table

Mapping of framework and local conditions to ZCX_HR_INFOTYPE_READER:

ConditionSeveritytextidexc_no
CL_HRPA_READ_INFOTYPE=>GET_INSTANCE raiseshardREADER_NOT_AVAILABLE10
No PA0001 record overlapping the input windowhardINVALID_PERNR1
BEGDA > ENDDA or either is initialhardINVALID_DATE_RANGE2
IT_INFOTYPES is emptyhardNO_INFOTYPES_REQUESTED3
Caller's structure doesn't match requested infotypeshardSTRUCTURE_MISMATCH4
CX_HRPA_VIOLATED_ASSERTION from READ / READ_WIDEhardREAD_FAILURE5
PRELP_TO_PNNNN_TAB assertionhardCONVERSION_FAILURE6
Mandatory infotype returned emptyhardMANDATORY_INFOTYPE_EMPTY7
MISSING_AUTH = 'X'warning(appended to ET_MESSAGES, not raised)n/a
Orphan secondary (no owning primary for MOLGA)warning(appended to ET_MESSAGES, not raised)n/a
Unknown T777D-INFKN valuewarning(appended to ET_MESSAGES, read proceeds as primary)n/a

Full definitions of each textid and exc_no are in exception_class_spec.md.


10. Reference — adapted skeleton

This is the internal read_one_infotype helper in pseudo-ABAP, combining everything above. It is not the public API — the public API is in the main class spec. This exists so implementers can see the full flow in one place.

METHOD read_one_infotype.
  " IMPORTING iv_pernr iv_infty iv_subty iv_begda iv_endda
  " CHANGING  ct_typed_tab (STANDARD TABLE OF P<NNNN>)
  " Uses instance attributes: mo_infty_reader, mv_molga, mt_prelp_cache,
  "                          mv_sprps, mv_no_auth_check, mt_messages

  " 1. Cache hit?
  READ TABLE mt_prelp_cache INTO DATA(ls_cached)
    WITH TABLE KEY pernr = iv_pernr infty = iv_infty.
  IF sy-subrc = 0.
    IF ls_cached-t_prelp IS NOT INITIAL.
      cl_hr_pnnnn_type_cast=>prelp_to_pnnnn_tab(
        EXPORTING prelp_tab = ls_cached-t_prelp
        IMPORTING pnnnn_tab = ct_typed_tab ).
      IF iv_subty IS NOT INITIAL.
        DELETE ct_typed_tab WHERE subty <> iv_subty.
      ENDIF.
    ENDIF.
    RETURN.
  ENDIF.

  " 2. Classify and resolve linkage
  DATA(ls_meta) = get_meta( iv_infty = iv_infty iv_molga = mv_molga ).

  CASE ls_meta-infkn.
    WHEN 'Z'.
      " Secondary: read via owning primary with READ_WIDE
      IF ls_meta-owning_primary IS INITIAL.
        append_warning( pernr = iv_pernr infty = iv_infty
                        text  = 'Orphan secondary; no owning primary' ).
        RETURN.
      ENDIF.
      do_wide_read( iv_pernr = iv_pernr
                    iv_primary_infty = ls_meta-owning_primary
                    iv_begda = iv_begda iv_endda = iv_endda ).
      serve_from_cache( iv_pernr iv_infty iv_subty CHANGING ct_typed_tab ).

    WHEN 'I'.
      IF ls_meta-secondary_infty IS NOT INITIAL.
        " Primary with linked secondary: READ_WIDE fills both cache entries
        do_wide_read( iv_pernr = iv_pernr
                      iv_primary_infty = iv_infty
                      iv_begda = iv_begda iv_endda = iv_endda ).
        serve_from_cache( iv_pernr iv_infty iv_subty CHANGING ct_typed_tab ).
      ELSE.
        " Standalone primary: READ
        do_narrow_read( iv_pernr = iv_pernr iv_infty = iv_infty
                        iv_subty = iv_subty
                        iv_begda = iv_begda iv_endda = iv_endda
                        CHANGING ct_typed_tab = ct_typed_tab ).
      ENDIF.

    WHEN OTHERS.
      " Unknown classification: warn and fall back to narrow read
      append_warning( pernr = iv_pernr infty = iv_infty
                      text  = |Unknown T777D-INFKN '{ ls_meta-infkn }'; treated as primary| ).
      do_narrow_read( iv_pernr = iv_pernr iv_infty = iv_infty
                      iv_subty = iv_subty
                      iv_begda = iv_begda iv_endda = iv_endda
                      CHANGING ct_typed_tab = ct_typed_tab ).
  ENDCASE.
ENDMETHOD.

do_wide_read and do_narrow_read mirror §3.1 and §3.2, with the single addition that both insert into mt_prelp_cache on success (and an empty entry on missing_auth to prevent retry).

Two code-level simplifications versus the skeleton above:

  • The subtype filter is applied once at the end of READ_ONE_INFOTYPE (DELETE et_prelp WHERE subty <> iv_subty) regardless of which dispatch branch fired, including the buffer-hit path. Callers therefore see the same post-filter result whether the read hit DB or cache.
  • The WHEN OTHERS branch emits a warning and falls back to a narrow READ — it does not re-classify or attempt wide-read recovery.

11. Things that can bite you

  • Decoupled infotypes: some infotypes under S/4HANA's decoupled framework have different storage backends. IF_HRPA_READ_INFOTYPE abstracts this — do not attempt SELECT * FROM PA<NNNN> directly.
  • Customer infotypes (9000–9999): often have no T582V/T582W entries. Classification may be blank; fall through to the WHEN OTHERS fallback.
  • Time infotypes (2001–2003): return raw records, not evaluated ones. If the caller needs evaluated absence balances, they need a different class (out of scope here).
  • SUBTY filter on READ_WIDE: not supported; filter after the cast. Mentioned in §8 but easy to miss.
  • BEGDA > ENDDA at framework level: no defined behavior — validate at the public-method entry point and raise INVALID_DATE_RANGE before any framework call.
  • Employees transferred between country groupings: if the employee's MOLGA changes within the requested window, the metadata resolution is ambiguous. Current policy: use MOLGA from the BEGDA-valid PA0001 record. Document this as a known limitation; revisit if real data requires it.
Built with LogoFlowershow