Section 1Overview
One framework that answers, for every table in the lakehouse: can this data be trusted?
The problem it solves. Without a shared framework, data quality checking is scattered: each pipeline hand-rolls its own validation (or has none), bad rows land silently in warehouse tables, and the first person to notice is a business user looking at a wrong number in a dashboard. There is no central record of what was checked, when, or what failed.
What the framework changes:
- Rules are defined once, centrally. A rule is a small config — table, column, rule type, severity, owner — stored in a Delta registry. Identical rules are deduplicated automatically, and rules can be deactivated without deleting history.
- Every run leaves an audit trail. Run logs, per-rule results, and the actual failing rows are persisted to Delta tables that dashboards can query directly.
- Bad data can be blocked, not just reported. Critical-severity failures come back as a DataFrame of the exact offending rows, so the pipeline can filter them out before writing (Section 4).
- Every rule declares an owner — accountability for data quality is written into the rule itself.
Section 2How it works
A pipeline calls one method — run_checks(table) — and the
engine does the rest.
add_rule() — table, column, rule type, severity, ownerrun_checks() always returns a complete
RunSummary — the pipeline stays in control and decides what to do with the
failures.Section 3Six dimensions, sixteen rule types
The industry-standard data quality dimensions, each covered by concrete, configurable rule types.
Completeness
3 rule typesIs anything missing?
null_checkrows where a column is null or emptymissing_partitionexpected date partitions that never arrivedvolume_checkrow count vs. a baseline thresholdValidity
4 rule typesAre values well-formed and in range?
regex_checkvalues not matching a format pattern (e.g. 13-digit ID card)allowed_valuesvalues outside an allowed listrange_checkvalues outside min/max boundstype_checkvalues that can't be cast to the expected typeAccuracy
3 rule typesDoes it match a trusted reference?
reference_matchrow-level mismatches against a reference tabletolerance_matchsame, but allowing a numeric tolerancereconciliationaggregate totals (sum/count) vs. the source systemConsistency
3 rule typesDoes it agree across tables and systems?
referential_integrityorphan rows missing from a reference tablecross_system_matchcolumn values that differ between systemsbusiness_ruleany SQL condition that should hold, e.g. end_date >= start_dateTimeliness
2 rule typesIs the data fresh?
freshness_checkhours of delay since the latest timestampsla_checkfails when the delay breaches an SLA thresholdUniqueness
1 rule typeAre there duplicates?
duplicate_checkduplicate groups on a set of key columnsSection 4The quarantine workflow
Two severity levels separate watch this from never let this through — and every load takes one of three paths.
How a load flows through the checkpoint
dq_failed_records, never reaching the target tableLive figures from lakehouse.data_quality as of 13 Aug 2026.
Warning — observe & investigate
- Result and pass/fail counts recorded on every run
- A sample of failing rows (up to 100 by default) saved for diagnosis
- The row still lands in the target table — a policy agreed with data owners; the trend is watched on dashboards
Critical — block & quarantine
- All failing rows saved to
dq_failed_records— no sampling cap - Failing rows returned as
RunSummary.critical_failed_rows_df - The pipeline diverts them to the quarantine zone — they never reach the target table
Filtering bad rows before an upsert
summary = engine.run_checks("my_catalog.my_schema.customers")
if summary.critical_failed_rows_df is not None:
clean_df = df.join(summary.critical_failed_rows_df, how="left_anti") # drop bad rows
clean_df.write.mode("overwrite").saveAsTable("my_catalog.my_schema.customers")
This is the framework's sharpest capability: quality checking stops being a report someone reads later and becomes a gate inside the pipeline — bad records are quarantined in the audit table while clean data flows through.
Section 5Delta audit tables
On first initialization the framework creates four Delta tables — the permanent record of what was checked and what failed.
| Table | What it holds |
|---|---|
dq_rule_registry | Every rule definition, with owner, severity, and an activation flag — rules are deactivated, never lost |
dq_run_log | One row per run: when it ran, how many rules passed and failed, total rows evaluated |
dq_result_table | Per-rule results for every run — pass rate, failed count, error details |
dq_failed_records | The failing rows themselves — all rows for critical rules, a sample for warnings |
DQEngine is initialized in a catalog — no DDL scripts, no
provisioning step.Section 6Monitoring views
Built-in aggregations turn the audit tables into dashboard-ready answers — no hand-written SQL required.
| Method | Question it answers |
|---|---|
daily_summary() | How did each table do today — runs, pass rates, failures by table and date? |
dimension_summary() | Which quality dimension is failing — is it a completeness problem or a freshness problem? |
rule_trend(days=30) | Is a specific rule getting better or worse over time? |
top_offenders(top_n=10) | Which rules have the lowest pass rates — where should the team focus first? |
Section 7Guardrails & scale
Engineering that keeps the framework safe to open up to many teams and many pipelines at once.
SQL guard — injection protection built in
Rules accept user-supplied SQL expressions, so every expression and identifier is
validated before it reaches Spark SQL. DDL/DML statements (DROP,
DELETE, MERGE, …), SQL comments, UNION SELECT,
and stored-procedure patterns are all blocked at add_rule() time with a
clear DQValidationError. Normal boolean expressions like
total_amount > 0 pass through untouched.
Row scoping with filter_expression new in 1.1.0
Any rule can be scoped to a subset of rows with a SQL filter — for example
COALESCE(is_deleted, false) = false checks only live rows and treats
soft-deleted ones as passing. It works on every rule type and goes through the same SQL
guard.
Safe parallel runs
Many pipelines can run checks concurrently against the same DQ tables: primary keys
are UUIDs generated in Python (no identity-column metadata conflicts), tables are
partitioned by target_table, and writes use
WriteSerializable isolation. This was hardened iteratively — versions
0.2.1 through 0.3.1 each removed a real concurrency failure seen in production.
Local time, local audit
All DQ timestamps are persisted in Asia/Bangkok time, so run logs and dashboards line up with the business day.
Section 8Developer experience
A pipeline adds full quality checking in about ten lines.
from ntb_dq_framework import DQEngine
engine = DQEngine(spark, catalog="my_catalog") # creates DQ tables if missing
engine.add_rule(
rule_name="null_email",
dimension="Completeness",
target_table="my_catalog.my_schema.customers",
target_column="email",
rule_type="null_check",
severity="critical",
owner="data-team",
)
summary = engine.run_checks("my_catalog.my_schema.customers")
print(summary.total_rows_passed, summary.total_rows_failed)
Ships with its own AI assistant
The package bundles a Claude Code agent: after pip install, one command —
ntb-dq install-agent — drops it into the project. The agent then activates
automatically whenever someone mentions DQEngine or a quality dimension,
and helps engineers design rules, write correct add_rule() calls, and
debug results.
Tested like a library, not a script
162 automated tests across ~2,700 lines of framework code — conventional unit tests plus property-based tests (Hypothesis) that probe invariants like row-count preservation and critical-severity handling with generated edge cases. Several past bugs have exploration-and-fix test pairs preserved in the suite.
Section 9Release history
Eight releases on PyPI — each one driven by real production use.
filter_expression lets any rule check only the rows that matter (e.g. skip soft-deleted records).ntb-dq install-agent CLI installs it per project or per user.critical_failed_rows_df introduced — pipelines can now filter bad rows out before writing. Tables partitioned for parallel runs.RunSummary and stays in control.