The fastest way to a useful test suite is not writing tests — it is profiling the table, letting an LLM propose tests from the profile, and then doing the genuinely human work: pruning the rules that would page you for normal data behavior.
This playbook covers the profile-propose-prune loop for dbt and Great Expectations suites, plus generating the synthetic edge-case fixtures that prove the tests fire when they should.
Use this when a model or table has no tests (the usual state of inherited pipelines), when onboarding a new source whose behavior you do not yet know, or when an incident reveals a class of check that should exist everywhere.
Triggers:
Profile with a query, not an impression. Generate the facts the proposals will be grounded in:
SELECT
COUNT(*) AS row_count,
COUNT(DISTINCT order_id) AS distinct_ids,
SUM(CASE WHEN customer_id IS NULL THEN 1 ELSE 0 END) AS null_customer,
MIN(order_date) AS min_date, MAX(order_date) AS max_date,
MIN(amount) AS min_amount, MAX(amount) AS max_amount,
COUNT(DISTINCT status) AS status_cardinality
FROM stg_orders;
-- plus: SELECT status, COUNT(*) FROM stg_orders GROUP BY status;
-- and daily row counts for the last 90 days for volume behavior
Propose tests from the profile — with reasons and false-positive risk.
Below: schema, profile stats, value distributions, and 90 days of
daily row counts for stg_orders.
Propose a dbt test suite (schema.yml) covering: uniqueness, nulls,
accepted values, ranges, referential integrity to stg_customers,
and freshness/volume expectations.
For EVERY proposed test include:
- the evidence in the profile that justifies it
- false-positive risk (low/medium/high) and what normal event would
trip it (month-end spike, new enum value, backfill)
- severity: error (block) vs warn.
Do not propose tests the profile cannot justify. Flag columns where
you need more history to say anything.
[paste profile output]
Prune like the on-call engineer you will be. This is the step that decides whether the suite helps or trains everyone to ignore alerts. Cut or downgrade: range tests pinned to the observed min/max (tomorrow's legitimate outlier fails), accepted-values on enums that grow (new product line = 3 a.m. page), volume checks with hard thresholds instead of tolerances. Keep hard error severity only where a violation genuinely means stop-the-pipeline: key uniqueness, critical non-nulls, referential integrity.
Prove the tests fire — with synthetic edge-case fixtures. A test you have never seen fail is a hope, not a check. Generate fixtures that violate each rule:
Generate 25 synthetic rows for stg_orders as CSV matching the schema:
- 15 valid rows with realistic variety (unicode names, boundary dates
like Feb 29, DST-transition timestamps, zero and max amounts)
- 10 violating rows, one per proposed test, each labeled in a
_violates column: duplicate order_id, null customer_id, status
outside accepted set, negative amount, order_date in the future,
customer_id absent from stg_customers, etc.
Foreign keys among valid rows must resolve against this fixture set.
Load the fixtures into a scratch schema, point the suite at it, and confirm: all 10 violations caught, all 15 valid rows pass. Any miss is a broken test — fix it now, while it costs nothing.
Install with ownership and routing. Every error-severity test gets an owner and a documented response ("uniqueness failure on order_id: stop, check upstream dedup job"). Warn-severity results route to a channel reviewed weekly, not to pages.
Schedule the re-profile. Data drifts; suites rot. A quarterly re-run of steps 1–3 against fresh history catches ranges that have shifted and enums that have grown before they page anyone.
The LLM proposes from history; history includes yesterday's undiscovered errors and excludes tomorrow's legitimate change. Every generated test is a hypothesis about the future — pruning is where your judgment enters the suite.
error-level test has a written justification for blocking the pipeline; everything else is warn.amount BETWEEN 0.01 AND 4999.99 because that was the observed range. Normal growth then generates failures until someone deletes the test in irritation.ship_date >= order_date) that actually catches corruption. Ask explicitly for cross-column and cross-table invariants.Profiles, not rows: the propose step needs aggregate statistics (counts, ranges, cardinalities, distribution shapes), which are far less sensitive than data — but on client engagements even value distributions of PII columns (names, cities) should be summarized as shapes ("varchar, 98% unique, unicode present"), never as value lists. Run proposal and fixture generation through client-tenancy Bedrock/Azure OpenAI or local Ollama; generate fixtures synthetically rather than sampling client rows. All profiling queries, fixture loads, and test runs execute inside the client warehouse, so no record ever appears in a prompt.