What it checks
One rule, applied to many columns: every value in a foreign key column must exist in its parent table. A row that fails this rule is an orphan, and orphans in a fact table mean revenue, claims, or transactions that no dimension can explain.
The rule's SQL lives on the template. The spreadsheet supplies what varies per column: which parent table and column to look up, and how many orphans are tolerated before the child test fails. That last part matters in practice. Late-arriving dimensions produce a known trickle of orphans in some feeds, and a rule with no allowance would fail every night for a reason nobody intends to fix.
Part of Template Test Examples: Import File.
The template
| Setting | Value |
|---|---|
| Name | Import File Example - Referential Integrity Rule |
| Level | Column |
| Generation source | Import File |
| Severity | High |
| Quality dimension | Consistency |
| Child test name | Orphans: {{table.name}}.{{column.name}} -> {{var.parent_table}}.{{var.parent_column}} |
| Child folder | Relative, {{var.template_test_name}} |
| Test data set | Script on the template's data source, single value, numeric (below) |
| Control data set | Numeric value: {{var.max_orphan_rows}} |
| Value success criteria | Custom calculation: testNumericValue <= controlNumericValue |
| Overall success | Value, tolerance 0 |
| Missing result actions | Fail on both sides |
The test data set SQL:
SELECT COUNT(*) AS ORPHAN_ROWS
FROM {{schema.name}}.{{table.name}} c
LEFT JOIN {{var.parent_schema}}.{{var.parent_table}} p
ON c.{{column.name}} = p.{{var.parent_column}}
WHERE c.{{column.name}} IS NOT NULL
AND p.{{var.parent_column}} IS NULL
Null foreign keys are excluded on purpose. A null is a different defect (a completeness problem) and belongs to a different test.
Variables declared on the template:
| Reference key | Type | Purpose |
|---|---|---|
parent_schema |
Schema | Schema of the parent table |
parent_table |
Table | Parent table |
parent_column |
Column | Column in the parent that the child key must match |
max_orphan_rows |
Numeric | Orphan rows allowed before the child test fails. Default 0 |
Nothing on the template is ticked From File. The allowance is a variable referenced directly in the control's Target Value field, the same way the drift templates shipped with Validatar carry their lookback windows. One mechanism for every per-row value, and a declared default to fall back on.
Two metadata links are set on the test data set. {{schema.name}}.{{table.name}}.{{column.name}} carries coverage for the foreign key column. {{var.parent_schema}}.{{var.parent_table}}.{{var.parent_column}} records the relationship to the parent without counting as coverage for it.
The import file
import-file-example-referential-integrity.xlsx, one sheet, header row plus 6 data rows.
| Header | Meaning |
|---|---|
schema.name, table.name, column.name |
The catalogued foreign key column. Column level, so all three are required |
test.key |
Unique key, written as CHILD.COLUMN -> PARENT for readability |
var.parent_schema, var.parent_table, var.parent_column |
Where the key must resolve |
var.max_orphan_rows |
Allowed orphans |
The rows:
| Foreign key column | Parent | Allowed orphans |
|---|---|---|
STAR_SCHEMA.FACT_SALES.CUSTOMER_KEY |
STAR_SCHEMA.DIM_CUSTOMER.CUSTOMER_KEY |
0 |
STAR_SCHEMA.FACT_SALES.PRODUCT_KEY |
STAR_SCHEMA.DIM_PRODUCT.PRODUCT_KEY |
0 |
STAR_SCHEMA.FACT_PAYMENT.POLICY_ID |
STAR_SCHEMA.DIM_POLICY.POLICY_ID |
0 |
STAR_SCHEMA.FACT_TRANSACTION.CLAIM_ID |
STAR_SCHEMA.DIM_CLAIM.CLAIM_ID |
6,000 |
STAR_SCHEMA.FACT_TRANSACTION.PAYMENT_ID |
STAR_SCHEMA.FACT_PAYMENT.PAYMENT_ID |
0 |
STAR_SCHEMA.DIM_CLAIM.POLICY_ID |
STAR_SCHEMA.DIM_POLICY.POLICY_ID |
0 |
The CLAIM_ID row shows the allowance in use. Claims arrive in FACT_TRANSACTION before DIM_CLAIM catches up, so a few thousand orphans are the accepted state and the threshold is set above it.
What materializes
Six child tests, one per foreign key column, in a folder named after the template. The PRODUCT_KEY child, for instance, gets this test script:
SELECT COUNT(*) AS ORPHAN_ROWS
FROM STAR_SCHEMA.FACT_SALES c
LEFT JOIN STAR_SCHEMA.DIM_PRODUCT p
ON c.PRODUCT_KEY = p.PRODUCT_KEY
WHERE c.PRODUCT_KEY IS NOT NULL
AND p.PRODUCT_KEY IS NULL
and a control value of 0. The CLAIM_ID child gets a control value of 6000.
Results on SAMPLE_DW
Verified 2026-09-11.
| Child | Orphans | Allowed | Result |
|---|---|---|---|
| FACT_SALES.CUSTOMER_KEY -> DIM_CUSTOMER | 0 | 0 | Pass |
| FACT_SALES.PRODUCT_KEY -> DIM_PRODUCT | 254 | 0 | Fail |
| FACT_PAYMENT.POLICY_ID -> DIM_POLICY | 0 | 0 | Pass |
| FACT_TRANSACTION.CLAIM_ID -> DIM_CLAIM | 5,045 | 6,000 | Pass |
| FACT_TRANSACTION.PAYMENT_ID -> FACT_PAYMENT | 0 | 0 | Pass |
| DIM_CLAIM.POLICY_ID -> DIM_POLICY | 0 | 0 | Pass |
The 254 sales rows with an unknown product are a real defect in the sample warehouse. They also demonstrate the value of the allowance column: set that row's var.max_orphan_rows to 254 and the test passes while the defect is being fixed, then set it back to 0 afterwards.
Adapting it
Cover your own star schema. One row per foreign key. The child column must be in the catalog; the parent named in the var. columns only needs to be queryable.
Parent in another schema or database. var.parent_schema is free text inside the SQL, so OTHER_DB.OTHER_SCHEMA works on platforms that allow three-part names.
Composite keys. The template joins on one column. For a two-column key, copy the template and add var.child_column_2 and var.parent_column_2 to the join, then add those headers to the file.
Turn the allowance into a percentage. Change the test SQL to return 100.0 * COUNT(orphans) / COUNT(*) and treat var.max_orphan_rows as a percent. The comparison stays <=.
Pitfall: column level means three required headers. Leave out column.name and the upload fails with Missing column: "column.name".
Pitfall: case on the parent names. They are pasted into SQL as written. Snowflake folds unquoted identifiers to upper case, so lower case works there, but match your platform's rules.