What it checks
Every value in a foreign key column must exist in its parent table. The rule and its SQL are the same as in Import File Example: Referential Integrity Rule.
The difference is who finds the foreign keys. In the file, someone listed six of them by hand. Here the script reads the warehouse's own INFORMATION_SCHEMA and applies a naming convention: a column named <ENTITY>_ID or <ENTITY>_KEY points at the table DIM_<ENTITY> when that table has a column of the same name. A new fact table that follows the convention is covered at the next materialization, with nobody editing a list.
A convention is never perfect, so the script also carries a short overrides list: an orphan allowance for one column, and one false match left out.
Part of Template Test Examples: Metadata Script.
The template
| Setting | Value |
|---|---|
| Name | Metadata Script Example - Referential Integrity Rule |
| Level | Column |
| Generation source | Metadata Script |
| Script data source | The template's own data source (SAMPLE_DW) |
| Everything else | Identical to the import file version: the orphan-count SQL, control value {{var.max_orphan_rows}}, the custom calculation testNumericValue <= controlNumericValue, and the same variables |
The metadata script
metadata-script-example-referential-integrity.sql:
-- Candidate foreign keys come from the warehouse's own catalog: any STAR_SCHEMA column
-- named <ENTITY>_ID or <ENTITY>_KEY whose table DIM_<ENTITY> has a column of the same name.
-- The overrides list covers what a naming convention cannot know.
WITH candidates AS (
SELECT c.table_schema, c.table_name, c.column_name,
p.table_schema AS parent_schema, p.table_name AS parent_table, p.column_name AS parent_column
FROM INFORMATION_SCHEMA.COLUMNS c
JOIN INFORMATION_SCHEMA.COLUMNS p
ON p.table_schema = c.table_schema
AND p.column_name = c.column_name
AND p.table_name = 'DIM_' || REGEXP_REPLACE(c.column_name, '_(ID|KEY)$', '')
WHERE c.table_schema = 'STAR_SCHEMA'
AND REGEXP_LIKE(c.column_name, '.*_(ID|KEY)')
AND c.table_name <> p.table_name
),
overrides AS (
SELECT column1 AS table_name, column2 AS column_name, column3 AS max_orphan_rows, column4 AS skip
FROM VALUES
('FACT_TRANSACTION', 'CLAIM_ID', 6000, FALSE), -- known backlog of claims not loaded yet
('FACT_REVENUE', 'POLICY_ID', 0, TRUE) -- no value matches DIM_POLICY: excluded until its owner confirms what it references
)
SELECT c.table_schema AS "schema.name",
c.table_name AS "table.name",
c.column_name AS "column.name",
c.table_name || '.' || c.column_name || ' -> ' || c.parent_table AS "test.key",
c.parent_schema AS "var.parent_schema",
c.parent_table AS "var.parent_table",
c.parent_column AS "var.parent_column",
TO_VARCHAR(COALESCE(o.max_orphan_rows, 0)) AS "var.max_orphan_rows"
FROM candidates c
LEFT JOIN overrides o
ON o.table_name = c.table_name AND o.column_name = c.column_name
WHERE NOT COALESCE(o.skip, FALSE)
ORDER BY "test.key"
How it works:
candidatesjoinsINFORMATION_SCHEMA.COLUMNSto itself. For eachSTAR_SCHEMAcolumn ending in_IDor_KEY, it strips the suffix, prefixesDIM_, and looks for a column with the same name in that table.FACT_PAYMENT.POLICY_IDbecomes a candidate becauseDIM_POLICY.POLICY_IDexists.c.table_name <> p.table_namestops a dimension from matching its own key.overridesis the part a person maintains. It holds only the exceptions, not the whole list.FACT_TRANSACTION.CLAIM_IDgets an allowance of 6,000 orphans for a known backlog of claims that load late.FACT_REVENUE.POLICY_IDis left out: none of its values matchDIM_POLICY, which suggests that the name follows the convention while the column refers to something else, until its owner says otherwise.test.keyisCHILD.COLUMN -> PARENT, the format the import file uses.var.max_orphan_rowscomes from the overrides list, or 0 for every other column.
What it returns
Seven rows: eight candidates, less the one override marked skip.
| Foreign key column | Parent | Allowed orphans |
|---|---|---|
DIM_CLAIM.POLICY_ID |
DIM_POLICY.POLICY_ID |
0 |
FACT_PAYMENT.POLICY_ID |
DIM_POLICY.POLICY_ID |
0 |
FACT_SALES.CUSTOMER_ID |
DIM_CUSTOMER.CUSTOMER_ID |
0 |
FACT_SALES.CUSTOMER_KEY |
DIM_CUSTOMER.CUSTOMER_KEY |
0 |
FACT_SALES.PRODUCT_ID |
DIM_PRODUCT.PRODUCT_ID |
0 |
FACT_SALES.PRODUCT_KEY |
DIM_PRODUCT.PRODUCT_KEY |
0 |
FACT_TRANSACTION.CLAIM_ID |
DIM_CLAIM.CLAIM_ID |
6,000 |
FACT_SALES has four foreign key columns, so that one table gets four child tests, one on each column.
Results on SAMPLE_DW
Verified 2026-10-19: 7 children, 6 pass, 1 fail.
| Child | Orphans | Allowed | Result |
|---|---|---|---|
| FACT_SALES.PRODUCT_KEY -> DIM_PRODUCT | 254 | 0 | Fail |
| FACT_TRANSACTION.CLAIM_ID -> DIM_CLAIM | 5,045 | 6,000 | Pass |
| The other five | 0 | 0 | Pass |
The PRODUCT_KEY failure is the same real defect the import file version finds.
Compared with the import file
| Import file | Metadata script | |
|---|---|---|
| How foreign keys are found | Listed by hand | Naming convention over INFORMATION_SCHEMA |
| What a person maintains | Every row | Only the exceptions |
| Child tests | 6 | 7 |
| Found by the script, not in the file | FACT_SALES.CUSTOMER_ID, FACT_SALES.PRODUCT_ID |
|
| In the file, missed by the convention | FACT_TRANSACTION.PAYMENT_ID -> FACT_PAYMENT (the parent is a fact table, not DIM_) |
The two lists disagree in both directions. A convention finds the columns nobody remembered to list, and misses the relationships that break the pattern. The overrides and an extra-pairs list (below) close the gap.
Adapting it
Use declared constraints instead. If your platform records foreign keys (INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS with KEY_COLUMN_USAGE on SQL Server and PostgreSQL, sys.foreign_keys on SQL Server), select the candidates from there and skip the naming convention.
Add the pairs a convention misses. UNION ALL a small VALUES list of extra pairs onto candidates, for example ('STAR_SCHEMA', 'FACT_TRANSACTION', 'PAYMENT_ID', 'STAR_SCHEMA', 'FACT_PAYMENT', 'PAYMENT_ID').
Keep the overrides in a table. Stewards can then raise an allowance or exclude a column without editing the template.
Pitfall: every candidate must be in the catalog. The script reads INFORMATION_SCHEMA, but the rows are matched against the Validatar catalog. A table created since the last metadata ingestion appears in the script and fails Preview & Validate with Invalid meta object. Ingest metadata first, or filter on tables the catalog holds.