10Testing · warehouse invariants
A unit test proves a transform is right for the rows someone thought of. An invariant is a property that must hold whatever ran, in whatever order, however many times — which is the situation you are actually in at 03:00 when a job is being retried by someone who did not write it. This route decides which properties are asserted after every load, and which of them are generated rather than hand-written per table.
Surviving a re-run
Every incident ends with someone re-running something. These three topics decide what that costs.
#Run it twice, assert nothing moved
from dwh.jobs import silver_customer
# Wall-clock only. If you have to add anything else to this list to make the
# test pass, you have found the bug, not the exception.
VOLATILE = ["_tech_loaded_at"]
def snapshot(spark, table: str) -> list:
return (
spark.read.table(table)
.drop(*VOLATILE)
.orderBy("customer_id", "_tech_valid_from")
.collect() # collect NOW — see the gotchas
)
def test_silver_customer_load_is_idempotent(spark, catalog):
silver_customer.run(spark, catalog)
first = snapshot(spark, f"{catalog}.silver.customer")
silver_customer.run(spark, catalog) # same bronze input, nothing new landed
second = snapshot(spark, f"{catalog}.silver.customer")
assert second == first_tech_loaded_at is genuinely wall-clock and genuinely differs. _tech_valid_from is not — if the second run moved it, the load re-versioned every customer, and the exclusion list is hiding the defect it was written to expose.gold.fct_order_line is loaded by MERGE on order_line_sk. Change that to an append — which someone will, because appends are faster and the sk is unique in the source — and a re-run doubles amount_eur for one day. Nothing errors. gold.agg_revenue_month is wrong by exactly one day's revenue, in a month, in a chart.Doing it
- Write the idempotency test before choosing the load pattern — it is the thing that rules out append.
- Compute _tech_hash over business columns only. A technical column inside the hash makes every row change on every run.
- Run every load twice in CI and assert a zero-row diff on the second pass.
- Keep the volatile-column list as one constant in one place, so widening it is a visible commit.
Defaults
- Every load runs twice in CI; the second run must change nothing.
- MERGE keyed on the surrogate key. INSERT is for append-only bronze and nothing else.
- _tech_hash covers business columns only, never technical ones.
- The exclusion list is one constant, reviewed like code.
Gotchas
- The lazy snapshot. If snapshot() returns a DataFrame instead of collected rows, the second run overwrites the table, Spark re-reads it, and both sides of the assertion are the after state. The test can never fail, and it will sit green in the suite for a year.
- _tech_loaded_at inside _tech_hash. Every row differs on every run, so SCD2 closes and reopens every interval nightly. dim_customer grows by one row per customer per day and nothing errors until someone asks how many versions a customer has.
- 'It is idempotent because we truncate and reload.' True until the source is a CDC feed that cannot be replayed, at which point the truncate is a data-loss event with a green job run.
- current_timestamp() evaluated inside the transform rather than passed in. Two tasks in one run get two different values, so even a single run is not reproducible and the idempotency test flaps.
#The retry that lands in the middle
gold.dim_customer, then lost an executor before gold.fct_order_line. The retry starts from the top with one table already at today's state and the rest at yesterday's. Test it by injecting the failure rather than by reasoning about it.import pytest
from dwh.jobs import gold_load
TABLES = ["gold.dim_customer", "gold.fct_order_line", "gold.agg_revenue_month"]
def fingerprint(spark, catalog: str) -> dict:
"""Order-independent content hash per table, ignoring load timestamps."""
out = {}
for t in TABLES:
row = spark.sql(f"""
SELECT count(*) AS n,
bit_xor(xxhash64(to_json(struct(*)))) AS h
FROM (SELECT * EXCEPT (_tech_loaded_at) FROM {catalog}.{t})
""").first()
out[t] = (row.n, row.h)
return out
class Boom(RuntimeError):
pass
def explode(*_args, **_kwargs):
raise Boom("simulated executor loss between tables")
def test_retry_after_partial_failure(spark, catalog, monkeypatch):
gold_load.run(spark, catalog)
clean = fingerprint(spark, catalog)
# die AFTER dim_customer is written, BEFORE fct_order_line
monkeypatch.setattr(gold_load, "build_fct_order_line", explode)
with pytest.raises(Boom):
gold_load.run(spark, catalog)
monkeypatch.undo()
gold_load.run(spark, catalog) # the retry, from the top
assert fingerprint(spark, catalog) == cleandim_customer already carries today's new SCD2 version. If build_fct_order_line resolves customer_sk by _tech_is_current instead of by the validity interval, the retry attaches last week's order lines to today's version. Revenue by segment shifts. No row is missing, no key is orphaned, and every test that does not compare against silver passes.Doing it
- Write each table with one atomic statement — a MERGE or an overwrite. Never a DELETE followed by an INSERT.
- Resolve dimension keys by interval: order_ts >= _tech_valid_from AND (_tech_valid_to IS NULL OR order_ts < _tech_valid_to).
- Keep the injection point a parameter of the test so you can move the failure between tables.
- Write the answer to 'is it safe to re-run blind?' into the runbook, per job, before go-live.
Defaults
- One atomic write per table per run.
- Retry-from-the-top is the default; resume-from-step needs a written justification.
- Failure injection lives in tests only — never a flag that exists in production code.
- Every job's runbook page answers the re-run question in its first three lines.
Gotchas
- DELETE then INSERT as two statements. The retry lands in the gap: the delete succeeded, the insert died, the table is short a day, and the retry is green. Every dashboard shows a dip nobody can attribute.
- Resolving customer_sk by _tech_is_current. It passes every test written against a single-version dimension, and starts returning wrong history in month three, in production, without an error.
- Retrying a job whose CDC source has already been consumed. The retry reads an empty feed, writes nothing, and reports success — the most expensive green run there is.
- Dropping count(*) from the fingerprint because 'the hash covers it'. bit_xor cancels in pairs: a row written twice XORs to zero, so a table loaded twice by a bad retry hashes identically to one loaded once. The count is what makes the fingerprint see the exact failure this test exists for.
- A fingerprint mismatch is not a diff. It tells you the tables differ, not how, and two engineers will spend an hour comparing bigints by eye. When it fails, run the symmetric EXCEPT from the SQL testing route — that is the tool that names the rows.
#MERGE and the second source match
bronze.sap_vbap is a CDC feed, so one order line can change three times inside a single batch. MERGE refuses that: if one target row matches more than one source row it fails the entire batch. The fix is not to loosen the merge condition — it is to deduplicate the source down to the merge grain, deterministically, before the merge sees it. Deterministically is the whole word: the ordering must come from SAP's own change counter, because every technical column bronze adds is constant within one batch.-- vbap_typed is the rename-only view from the ingestion route: bronze keeps
-- SAP's field names (VBELN, POSNR, MATNR, …), the view maps them to canonical
-- ones and passes change_seq and the _tech_ columns through untouched.
MERGE INTO silver.sales_order_line AS t
USING (
SELECT order_id, line_number, product_id, quantity, unit_price, currency, status
FROM (
SELECT *,
row_number() OVER (
PARTITION BY order_id, line_number
-- SAP's change counter, NOT _tech_loaded_at: every row in this
-- batch shares the load time. See the gotchas.
ORDER BY change_seq DESC
) AS rn
FROM vbap_typed
WHERE _tech_batch_id = :batch_id
)
WHERE rn = 1
) AS s
ON t.order_id = s.order_id
AND t.line_number = s.line_number
WHEN MATCHED THEN UPDATE SET
t.product_id = s.product_id,
t.quantity = s.quantity,
t.unit_price = s.unit_price,
t.currency = s.currency,
t.status = s.status,
t._tech_loaded_at = current_timestamp()
WHEN NOT MATCHED THEN INSERT
(order_id, line_number, product_id, quantity, unit_price, currency,
status, _tech_loaded_at)
VALUES
(s.order_id, s.line_number, s.product_id, s.quantity, s.unit_price,
s.currency, s.status, current_timestamp())WHEN MATCHED AND … condition that happens to reduce the match to one row. The merge stops failing and starts keeping an arbitrary version of the line — arbitrary meaning it can differ between two runs over identical input, which quietly destroys idempotency as well.Doing it
- Deduplicate in the USING subquery with row_number(), on every merge, including the ones where duplicates 'cannot' happen.
- Make the dedup partition identical to the merge ON clause. If they differ, one of them is wrong.
- Assert the grain of the deduplicated source as a zero-rows check before the merge runs.
- Keep a double-change row in the fixtures schema permanently, so the case can never regress out of the suite.
Defaults
- Every MERGE source is a deduplicated subquery with an explicit deterministic ORDER BY.
- The dedup key equals the merge key, always.
- A source-grain assertion runs in CI for every merge in the repository.
- Seeded duplicate rows live in fixtures forever, not just during the sprint that found them.
Gotchas
- Deduplicating with DISTINCT. It removes rows identical in every column; two CDC versions of one line differ in status, so both survive and the merge fails anyway — after the author has convinced themselves it is handled.
- Ordering the row_number() by _tech_loaded_at or _tech_batch_id. Both are constant inside one batch — the batch filter guarantees it — so the tie is broken by whatever the shuffle put last. The merge succeeds every time and two runs over identical input produce two different silver tables, which shows up as an idempotency test that flaps rather than as a merge error. Order by change_seq, the source's own counter, which bronze carries unmodified.
- Adding a column to the merge key to make the error go away — keying on order_id, line_number, status. It never fails again, and it inserts a new row per status change, so 'one row per order line' is gone and every downstream count is inflated.
- Assuming AUTO CDC removes the problem. It does the same deduplication for you, and it needs the same sequencing column; without a reliable one it picks a winner just as arbitrarily.
SCD2 boundary cases
Seven cases, one parametrised file. Each produces a dimension that looks correct and answers a business question differently.
#The seven cases, and the two assertions that catch all of them
| Case | What arrives | Correct result | What breaks if it is wrong |
|---|---|---|---|
| No change | the same daily bronze.sap_kna1 extract twice | one version, _tech_valid_from unchanged, one open interval | a new version every night; the dimension grows by one row per customer per day |
| Segment change | customer_segment A to B (rule 4) | two versions; the first closed at the second's _tech_valid_from | the old segment is overwritten and last quarter's revenue by segment silently changes |
| Address change | street changes, segment does not (rule 4) | still one version, updated in place | a version per address correction; as-of joins fan out over versions carrying no business change |
| Reopening | segment A, then B, then A again | three versions; the third is new, not a revived first | hash-lookup code reuses the old row and the B interval becomes unattributable |
| Twice in one day | extracts at 06:00 and 18:00, both changing the segment | two versions with strictly increasing TIMESTAMP _tech_valid_from | a DATE-granularity clock gives _tech_valid_from = _tech_valid_to; the as-of join matches nothing and the line falls to the UNKNOWN member |
| Late arrival | the 01.03 extract processed after the 02.03 one | the existing interval is SPLIT, not appended to | overlapping intervals; the as-of join returns two dimension rows and revenue doubles for that customer |
| Deletion | the customer stops appearing in the full extract | interval closed, _tech_is_current = false, row retained | a hard delete orphans every historical fact row; last year's numbers change retroactively |
import datetime as dt
import pytest
from dwh.common.scd2 import apply_scd2
KEYS = ["customer_id"]
SCHEMA = "customer_id STRING, customer_segment STRING, street STRING"
def T(day: int, hour: int) -> dt.datetime:
return dt.datetime(2026, 3, day, hour)
def cust(segment="A", street="Bahnhofstr. 1"):
return {"customer_id": "C1", "customer_segment": segment, "street": street}
CASES = [
# name (as_of, extract) in processing order versions open
("no-change", [(T(1, 6), [cust()]), (T(2, 6), [cust()])], 1, 1),
("segment-change", [(T(1, 6), [cust()]), (T(2, 6), [cust("B")])], 2, 1),
("address-only", [(T(1, 6), [cust()]),
(T(2, 6), [cust(street="Domplatz 4")])], 1, 1),
("reopen-A-B-A", [(T(1, 6), [cust()]), (T(2, 6), [cust("B")]),
(T(3, 6), [cust()])], 3, 1),
("twice-one-day", [(T(1, 6), [cust()]), (T(1, 18), [cust("B")])], 2, 1),
("late-arrival", [(T(2, 6), [cust("B")]), (T(1, 6), [cust()])], 2, 1),
("deleted", [(T(1, 6), [cust()]), (T(2, 6), [])], 1, 0),
]
@pytest.mark.parametrize("case,extracts,versions,open_rows", CASES)
def test_scd2_boundary(spark, case, extracts, versions, open_rows):
state = None
for as_of, extract in extracts:
state = apply_scd2(
state, spark.createDataFrame(extract, SCHEMA), KEYS, as_of=as_of
)
rows = sorted(
state.where("customer_id = 'C1'").collect(),
key=lambda r: r._tech_valid_from,
)
assert len(rows) == versions
assert sum(1 for r in rows if r._tech_valid_to is None) == open_rows
# the two assertions that catch every case above, whatever the counts say
assert all(r._tech_valid_from < (r._tech_valid_to or dt.datetime.max)
for r in rows) # no zero-length
assert all(a._tech_valid_to == b._tech_valid_from
for a, b in zip(rows, rows[1:])) # no gap, no overlapgold.fct_order_line resolves customer_sk by validity interval, so overlapping intervals return two dimension rows for one fact row and revenue doubles for exactly the customers who changed. A zero-length interval does the opposite: the join matches nothing and the line lands on the UNKNOWN member (rule 7), where most dashboards filter it out of sight.Doing it
- Write these seven cases before writing apply_scd2. They are the specification, and the argument about case three is the modelling meeting.
- Assert contiguity and non-zero length in every case, not only the version counts.
- Treat _tech_valid_to as exclusive, say so in the column comment, and test a fact timestamped exactly on the boundary — it belongs to the later version.
- Inject the clock as as_of. A transform that calls current_timestamp() cannot be tested for any of these cases.
- Close intervals on deletion; never hard-delete from a dimension. Erasure requests are a different mechanism entirely.
Defaults
- Seven cases, parametrised, one file, run on every commit.
- Exclusive end, NULL = open, asserted in every case.
- Clock injected as a parameter; current_timestamp() never called inside a transform.
- Deletion closes the interval and sets _tech_is_current = false.
- _tech_hash covers only the versioned attributes, so rule 4 is enforced by the hash rather than by an if-statement someone can delete.
Gotchas
- An inclusive _tech_valid_to. The fact row on the boundary matches two versions, the join fans out, and revenue for that customer doubles on exactly the day they changed segment. It is a small enough error to survive until year-end reconciliation.
- Zero-length intervals from a DATE-granularity clock when two extracts land on the same day. Revenue does not disappear — it moves to the UNKNOWN member, and 'unknown' is the bar most dashboards hide.
- Late extracts appended instead of splitting the interval. This is the longest-lived defect of the seven because it needs an out-of-order load to appear at all, so it survives every test run in a well-behaved test environment.
- Hashing all columns into _tech_hash. Address corrections then create versions, rule 4 is violated in the direction nobody checks, and the dimension grows without a single error being raised.
Generated structure, and the one test the business can read
Tests that must exist for tables nobody has built yet, and the check that catches everything the other tests cannot.
#Uniqueness, orphans and grain, generated
SELECT tc.table_schema,
tc.table_name,
array_join(sort_array(collect_list(kcu.column_name)), ', ') AS pk_columns
FROM {catalog}.information_schema.table_constraints tc
JOIN {catalog}.information_schema.key_column_usage kcu
ON kcu.constraint_catalog = tc.constraint_catalog
AND kcu.constraint_schema = tc.constraint_schema
AND kcu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema IN ('silver', 'gold')
GROUP BY tc.table_schema, tc.table_name
ORDER BY 1, 2import os
from functools import lru_cache
from dwh.testing.constraints import FK_QUERY, PK_QUERY
from dwh.testing.session import metadata_session # pytest fixtures do not
# exist at collection time
# The generator and the tests MUST read the same catalog: the "catalog"
# fixture resolves from this same variable. See the gotchas.
CATALOG = os.environ["DWH_CATALOG"]
@lru_cache(maxsize=1)
def _declared():
"""Read once at collection: (primary keys, foreign keys)."""
with metadata_session() as spark:
return ([tuple(r) for r in spark.sql(PK_QUERY.format(catalog=CATALOG)).collect()],
[tuple(r) for r in spark.sql(FK_QUERY.format(catalog=CATALOG)).collect()])
def pytest_generate_tests(metafunc):
pks, fks = _declared()
if "pk" in metafunc.fixturenames:
metafunc.parametrize("pk", pks, ids=[f"{s}.{t}" for s, t, _ in pks])
if "fk" in metafunc.fixturenames:
metafunc.parametrize("fk", fks, ids=[f"{c}.{col}" for c, col, _, _ in fks])
def test_declared_pk_is_unique(spark, catalog, pk):
schema, table, cols = pk
dupes = spark.sql(f"""
SELECT {cols} FROM {catalog}.{schema}.{table}
GROUP BY {cols} HAVING count(*) > 1
""").count()
assert dupes == 0, f"{schema}.{table}: {dupes} duplicate {cols} groups"
def test_declared_fk_has_no_orphans(spark, catalog, fk):
child, child_col, parent, parent_col = fk
orphans = spark.sql(f"""
SELECT c.{child_col}
FROM {catalog}.{child} c
LEFT ANTI JOIN {catalog}.{parent} p ON p.{parent_col} = c.{child_col}
WHERE c.{child_col} IS NOT NULL
""").count()
assert orphans == 0, f"{child}.{child_col} -> {parent}: {orphans} orphans"
def test_every_gold_table_declares_a_grain(spark, catalog):
missing = spark.sql(f"""
SELECT t.table_name
FROM {catalog}.information_schema.tables t
LEFT ANTI JOIN {catalog}.information_schema.table_constraints tc
ON tc.table_schema = t.table_schema
AND tc.table_name = t.table_name
AND tc.constraint_type = 'PRIMARY KEY'
WHERE t.table_schema = 'gold'
-- gold.mv_sales is a metric view and cannot carry a PK
AND t.table_type IN ('MANAGED', 'EXTERNAL')
""").collect()
assert not missing, f"no declared grain: {[r.table_name for r in missing]}"FK_QUERY lives beside PK_QUERY in the same module and is the same shape, joining referential_constraints to key_column_usage twice to yield child table, child column, parent table, parent column. Between the three tests, a table added next month by someone who has never read this handbook is covered the moment its DDL declares a key — which is a materially different property from a suite that covers the tables somebody remembered.order_line_sk is sha2(concat_ws('||', order_id, line_number), 256). A CDC duplicate that slipped past the merge dedup produces two rows with the same surrogate key, and Unity Catalog accepts it without comment. The generated uniqueness test is the only thing standing between that and a doubled revenue figure.RELY raises the stakes: it tells the optimizer to trust the constraint, so a violated RELY constraint does not merely fail to help — it lets the planner drop a deduplication it believes is redundant and return wrong results faster.Doing it
- Declare PKs and FKs in DDL even though they enforce nothing. You are writing the test specification, not a constraint.
- Generate at collection time so every table gets its own named, individually reported test.
- Run the generated suite against dwh_test after every load and against dwh_prod nightly.
- Assert the share of fact rows on the UNKNOWN member alongside the orphan check, with a threshold.
- Add RELY only to constraints the generated suite actually covers.
Defaults
- A declared PK on every silver and gold table; declared FKs on every fact.
- Tests generated from information_schema, never hand-copied per table.
- Orphan test and UNKNOWN-share threshold shipped together, never separately.
- RELY only where the generated test runs on every load.
Gotchas
- Reading the DDL and assuming enforcement. The declaration looks exactly like a database constraint, so teams skip the test — which is the precise combination that lets a duplicate reach a dashboard.
- Parametrising from a fixture. pytest fixtures do not exist at collection time, so the metadata query must run in pytest_generate_tests; otherwise the suite collapses into one opaque test that reports the first failure and hides every other.
- Zero orphans from a broken join. Join on the wrong key and every fact row resolves to the UNKNOWN member, which is a valid dimension row — the orphan test is green and the segment chart is one enormous 'unknown' bar.
- Generating against production metadata and running against test data — the shape this takes in practice is dwh_prod hardcoded in the metadata query while the tests use the catalog fixture. Tables that exist only in prod then produce 'table not found' failures, and a suite that is noisy on day one is a suite that is ignored by day thirty.
- Running the grain test over information_schema.tables unfiltered. gold.mv_sales is a metric view and views cannot declare a primary key, so the test goes red on an object that is entirely correct — and a test that is red for a good reason gets muted within a fortnight, taking the coverage of every real gold table with it.
#Reconciliation across layers
WITH silver_revenue AS (
SELECT date_trunc('MONTH', o.order_ts) AS month,
-- EUR is the base currency and is absent from the ECB file, so it is
-- 1 by definition. Any OTHER currency with no rate must NOT vanish
-- into sum()'s NULL-skipping: it is counted on the next line.
sum(round(l.quantity * l.unit_price *
CASE WHEN l.currency = 'EUR' THEN CAST(1 AS DECIMAL(18,6))
ELSE f.fx_rate END, 2)) AS amount_eur,
count_if(l.currency IS DISTINCT FROM 'EUR'
AND f.fx_rate IS NULL) AS lines_without_rate
FROM silver.sales_order_line l
JOIN silver.sales_order o ON o.order_id = l.order_id
LEFT JOIN silver.fx_rate f ON f.currency = l.currency
AND f.rate_date = date(o.order_ts)
WHERE l.status IS DISTINCT FROM 'CANCELLED' -- rule 1, NULL-safe
GROUP BY 1
),
gold_revenue AS (
SELECT date_trunc('MONTH', order_ts) AS month,
sum(amount_eur) AS amount_eur
FROM gold.fct_order_line
WHERE status IS DISTINCT FROM 'CANCELLED'
GROUP BY 1
)
SELECT coalesce(s.month, g.month) AS month,
coalesce(s.amount_eur, 0) AS silver_eur,
coalesce(g.amount_eur, 0) AS gold_eur,
coalesce(g.amount_eur, 0) - coalesce(s.amount_eur, 0) AS delta,
coalesce(s.lines_without_rate, 0) AS lines_without_rate
FROM silver_revenue s
FULL OUTER JOIN gold_revenue g ON g.month = s.month
-- rule 2 rounds per line on both sides from the same three DECIMAL columns, so
-- an exact match is the expectation and one cent is pure arithmetic noise.
-- Anything larger is a difference, not drift.
WHERE abs(coalesce(g.amount_eur, 0) - coalesce(s.amount_eur, 0)) > 0.01
-- a missing rate makes BOTH sides smaller by the same amount, so the delta
-- stays zero and the defect is invisible. This is the row that shows it.
OR coalesce(s.lines_without_rate, 0) > 0
ORDER BY 1gold.fct_order_line that dropped four percent of its lines in a join passes every structural test in this route. The concrete mechanism is right there in the query: inner-join silver.fx_rate and a missing currency-day removes those lines from gold entirely. No error, no orphan, no duplicate — a smaller number. Reconciliation is the only check that sees it.sum() skips the same NULL rate agree perfectly on a wrong number — delta is 0,00 and the check is green. lines_without_rate is the column that refuses to let both sides be wrong in the same direction, and it is the reason the tolerance can stay at one cent.Doing it
- Write one reconciliation per business rule that crosses a layer boundary: revenue, line count, distinct customers.
- Reconcile counts as well as sums — a dropped line and a duplicated line net to a matching total.
- Slice by country and currency; a matching total hides a DE surplus offsetting a CH shortfall.
- Run it after every load and let it block the downstream refresh, not on a nightly schedule of its own.
- State the tolerance and its arithmetic reason in a comment, next to the number.
Defaults
- Reconciliation runs after every load and gates the refresh.
- Zero rows means pass; the output is the differing months and the delta, not a boolean.
- Tolerance is a stated number with a stated cause, changed only by a commit that explains itself.
- Sums, counts and distinct-key counts, sliced by country and currency.
- The expected figure starts from silver — never from the table under test.
- Every unresolved lookup is counted as its own column. A reconciliation that can only report a delta cannot report the failures that shrink both sides equally.
Gotchas
- Tolerance creep. The threshold gets widened once per incident until it is comfortably larger than the next real defect. A tolerance that has moved twice is a bug that has been renamed.
- Reconciling totals only. One dropped DE line and one duplicated CH line produce a matching total and two obvious failures the moment you slice by country.
- Inner-joining silver.fx_rate in the gold job. A missing rate for one currency-day silently deletes those lines from gold. The disguised version is worse: left-join in the reconciliation too, and sum() skips the NULL on the silver side as well, so both sides lose the same revenue and the delta is a confident 0,00. Left-join AND count the unresolved rates.
- Forgetting that EUR has no ECB rate. It is the base currency, so it is absent from the file by design; a bare left join gives every DE and AT line a NULL fx_rate and the reconciliation reports most of the book missing. Someone then widens the tolerance to make the noise stop, and the tolerance is now large enough to hide a real defect.
- Deriving the expected figure from gold. That proves the SQL runs. The comparison must start in silver, and ideally be written by someone who did not write the gold job.
- Running it nightly while the dashboard refreshes at 06:00. The business finds the error first, which costs more trust than the error itself.
Straight to the documentation
The generic platform material lives in the official docs and is not restated here. These are the pages worth having open while you work through this route.
- Upsert into a Delta Lake table using merge — states the multiple-source-match rule and shows the deduplication pattern — read before writing your first MERGE
- MERGE INTO reference — the exact semantics of WHEN MATCHED conditions, which is what people reach for when the merge starts failing
- Constraints on Databricks — says plainly that primary and foreign keys are informational, and what RELY changes for the optimizer
- Information schema — the metadata the generated uniqueness, orphan and grain tests read
- Work with Delta Lake table history — after a suspicious retry, DESCRIBE HISTORY tells you which operation actually wrote the current version