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)
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
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: trueoperator (e.g.not_equal_to,equal_to) resolves a dynamic column name from a variable likeIDVAR, and that value does not match an actual column in the target dataset,_check_inequality(and the equivalent_check_equality) raises an unhandledKeyError. 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:The exception is caught much further up the call stack in
rules_engine.py(handle_validation_exceptions), which special-casesKeyErrorasSkippedReason.COLUMN_NOT_FOUND_IN_DATAand returns aFailedValidationEntityfor 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:Given a
COdataset with:...and an
LBdataset with a single row whereLBSEQ=299(soLBSEQ=320doesn't exist, andNONSENSEisn't a real LB column):Run:
Expected: at minimum, the
LBSEQ=320row should be reported as a violation (IDVARVAL=320 doesn't exist in LB.LBSEQ), independent of what happens with theNONSENSErow.Actual: the CSV report contains zero rows for
CO. Debug log shows:The single bad row (
IDVAR=NONSENSE) takes down validation for the entireCOdataset, causing the genuinely-violatingLBSEQ=320row to go unreported as well. The same happens for aRELRECdataset with the same shape. A siblingSUPPLBdataset with noNONSENSE-style row validates and reports correctly, confirming the failure is isolated to datasets containing a row whoseIDVARdoesn't resolve to a real column.Impact
Any rule using
value_is_reference: trueto dynamically resolve a column name from another variable (a common pattern forRDOMAIN/IDVAR/IDVARVALrelationship checks acrossSUPP--,CO, andRELREC) 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 theKeyErrorescape:This keeps a single bad reference from aborting the whole dataset, and (depending on desired semantics) could itself be reported as a finding, since
IDVARVALclearly cannot equal a value of a variable that doesn't exist in the target domain.Environment
cdisc_rules_engineversion: 0.16.0engine/core.py validatedirectly (not just throughtest.py)