Skip to content

unhandled KeyError #1824

Description

@pendingintent

Standard

Custom

Reference Rule ID(s)

TIG0058

Conformance Rule ID(s) (if published in CORE)

CORE-000206

JIRA Ticket

https://jira.cdisc.org/projects/CORERULES/issues/CORERULES-

CLI Command Used (if applicable)

python core.py -s SDTM -v 3.4 -d /path/to/datasets

Rule YAML

Authorities: 
  - Organization: 
    Standards:
      - Name: 
        References:
          - Citations:
              - Cited Guidance: 
                Document: 
                Item: 
                Section: 
            Origin: 
            Rule Identifier: 
              Id: 
              Version: 
            Version: 
        Version:
Check:
  all:
    - name: 
      operator: 
      value: 
Core:
  Id: 
  Status: 
  Version: '1'
Description: 
Executability: Fully Executable
Match Datasets:
  - Keys:
      - 
    Name: 
Operations:
  - id: 
    operator: 
Outcome:
  Message: 
  Output Variables:
    -
Rule Type: 
Scope:
Sensitivity:

Attach any sample test data file(s)

_datasets.csv
_variables.csv
co.csv
lb.csv
relrec.csv
supplb.csv

Attach any output report and/or log file(s)

results(1).csv

Expected output

results.csv

Any Additional Information

Summary

When a value_is_reference: true operator (e.g. not_equal_to, equal_to) resolves a dynamic column name from a variable like IDVAR, and that value does not match an actual column in the target dataset, _check_inequality (and the equivalent _check_equality) raises an unhandled KeyError. That exception propagates out of the row-wise .apply() call and aborts validation for the entire dataset, not just the offending row — so every other, otherwise-valid finding in that dataset is silently dropped from the report.

Where

cdisc_rules_engine/check_operators/dataframe_operators.py, _check_inequality:

if value_is_reference:
    dynamic_column_name = row[comparator]
    comparison_data = row[dynamic_column_name]   # <-- KeyError if dynamic_column_name isn't a real column

The exception is caught much further up the call stack in rules_engine.py (handle_validation_exceptions), which special-cases KeyError as SkippedReason.COLUMN_NOT_FOUND_IN_DATA and returns a FailedValidationEntity for the whole dataset — there is no per-row recovery, so the dataset is marked as "skipped" instead of reporting the rows that did fail the check.

Steps to reproduce

Using rule CORE-000206 ("When IDVAR is populated, IDVARVAL must equal a value of the variable referenced by IDVAR within the domain referenced by RDOMAIN"), which uses:

Check:
  all:
    - name: IDVAR
      operator: non_empty
    - name: IDVARVAL
      operator: non_empty
    - name: IDVARVAL
      operator: not_equal_to
      type_insensitive: true
      value: IDVAR
      value_is_reference: true

Given a CO dataset with:

RDOMAIN USUBJID IDVAR IDVARVAL
LB S001 LBSEQ 320
LB S001 NONSENSE 299

...and an LB dataset with a single row where LBSEQ=299 (so LBSEQ=320 doesn't exist, and NONSENSE isn't a real LB column):

Run:

python3 core.py validate -lr CORE-000206.yml -d <data_dir> -dep <env> -of CSV -o out -l debug

Expected: at minimum, the LBSEQ=320 row should be reported as a violation (IDVARVAL=320 doesn't exist in LB.LBSEQ), independent of what happens with the NONSENSE row.

Actual: the CSV report contains zero rows for CO. Debug log shows:

[ERROR] - No IDVAR match found for LBSEQ=320
[ERROR] - No IDVAR match found for NONSENSE=299
[ERROR] - Error in not_equal_to: 'NONSENSE', traceback: ...
KeyError: 'NONSENSE'
...
[ERROR] - Error occurred during validation.
            Error: 'NONSENSE'
            Error Type: <class 'KeyError'>
            Dataset Name: CO
            Rule ID: CORE-000206

The single bad row (IDVAR=NONSENSE) takes down validation for the entire CO dataset, causing the genuinely-violating LBSEQ=320 row to go unreported as well. The same happens for a RELREC dataset with the same shape. A sibling SUPPLB dataset with no NONSENSE-style row validates and reports correctly, confirming the failure is isolated to datasets containing a row whose IDVAR doesn't resolve to a real column.

Impact

Any rule using value_is_reference: true to dynamically resolve a column name from another variable (a common pattern for RDOMAIN/IDVAR/IDVARVAL relationship checks across SUPP--, CO, and RELREC) will silently drop all findings for a dataset if even one row has a dangling/invalid reference value — with no findings and no visible error surfaced in the CSV/report output, this looks like a false negative rather than an engine error.

Suggested fix

In _check_inequality (and _check_equality), guard the dynamic lookup and treat an unresolvable reference as a per-row outcome rather than letting the KeyError escape:

if value_is_reference:
    dynamic_column_name = row[comparator]
    if dynamic_column_name not in row.index:
        comparison_data = None   # or treat as automatically "not equal" per the check's semantics
    else:
        comparison_data = row[dynamic_column_name]

This keeps a single bad reference from aborting the whole dataset, and (depending on desired semantics) could itself be reported as a finding, since IDVARVAL clearly cannot equal a value of a variable that doesn't exist in the target domain.

Environment

  • cdisc_rules_engine version: 0.16.0
  • Python 3.12.11
  • Reproduced via engine/core.py validate directly (not just through test.py)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

TIGtestingUnit, regression, performance, QA, test automation

Type

Fields

Priority

None yet

T-shirt sizing

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions