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)

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