Stop Hardcoding Database Checks: Building a Metadata-Driven Data Quality Framework
Decouple validation from code. Learn how to build a dynamic, metadata-driven data quality framework using Databricks, Snowflake, and Python.
Join the DZone community and get the full member experience.
Join For FreeIn high-volume data platforms, hardcoding validation logic into individual processing pipelines creates significant operational drag. As an enterprise data asset footprint grows, maintaining manual checks for hundreds of tables inevitably leads to mounting technical debt, silent schema drift, and a fragmented audit trail.
To achieve data governance at scale, data architects must decouple validation rules from the execution engine. By utilizing a centralized metadata repository to dynamically generate validation suites, organizations can transform data quality from a reactive, script-based bottleneck into a configuration-driven infrastructure asset.
The Metadata-Driven Architecture
Instead of embedding validation constraints directly inside an ETL/ELT pipeline, this pattern isolates validation rules inside a centralized relational database schema. The orchestration engine programmatically queries this metadata at runtime, constructs the validation suites on the fly, executes them against target tables, and routes the evaluation metrics to an observability layer.
This architecture provides three primary engineering advantages:
- Decoupled Governance: Data stewards can alter business rules or add expectations via simple DML updates without modifying or redeploying production application code.
- Schema Drift Resilience: The engine dynamically adapts to structural variations by programmatically evaluating target datasets against rules defined at the column level.
- Centralized Observability: Every rule execution generates a standardized, traceable metric payload, laying a consistent foundation for real-time data auditing and data lineage maps.
1. Defining the Metadata Schema (DDL)
To implement this framework in an enterprise Lakehouse ecosystem, the metadata table must act as an immutable source of truth for constraints. Below is the production DDL required to initialize the control directory in Snowflake or Databricks:
CREATE TABLE data_quality_rules (
rule_id INT IDENTITY(1,1),
table_name VARCHAR(255) NOT NULL,
column_name VARCHAR(255) NOT NULL,
expectation_type VARCHAR(255) NOT NULL,
expectation_kwargs VARIANT NOT NULL, -- Stored as JSON object
is_active BOOLEAN DEFAULT TRUE,
updated_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP(),
CONSTRAINT pk_rule_id PRIMARY KEY (rule_id)
);
-- Seed metadata rules for execution tracking
INSERT INTO data_quality_rules (table_name, column_name, expectation_type, expectation_kwargs)
VALUES
('CUSTOMERS', 'CUST_ID', 'expect_column_values_to_not_be_null', '{}'),
('CUSTOMERS', 'AGE', 'expect_column_values_to_be_between', '{"min_value": 18, "max_value": 60}'),
('ORDERS', 'ORDER_ID', 'expect_column_values_to_not_be_null', '{}');
2. Implementation: The Programmatic Execution Engine
The core execution wrapper leverages Python and Great Expectations (gx) to programmatically turn rows of metadata into active validation suites. This script establishes a secure database connection via SQLAlchemy, harvests active constraints, generates runtime batch requests, and triggers structured checkpoints.
import os
import json
import logging
from datetime import datetime
import pandas as pd
from sqlalchemy import create_engine
import great_expectations as gx
from great_expectations.core.batch import RuntimeBatchRequest
# Configure structured logging for production auditing
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class MetadataDataQualityEngine:
def __init__(self, connection_string: str):
self.engine = create_engine(connection_string)
# Initialize Great Expectations ephemeral context for programmatic runtime control
self.context = gx.get_context(context_root_dir=None)
def fetch_active_metadata(self) -> pd.DataFrame:
"""Harvests active validation configurations from the centralized store."""
query = """
SELECT table_name, column_name, expectation_type, expectation_kwargs
FROM data_quality_rules
WHERE is_active = TRUE
"""
try:
df = pd.read_sql(query, self.engine)
logger.info(f"Successfully harvested {len(df)} active validation rules.")
return df
except Exception as e:
logger.error(f"Failed to query metadata repository: {str(e)}")
raise
def compile_expectation_suite(self, table_name: str, rules_df: pd.DataFrame):
"""Assembles validation rules into a Great Expectations suite on the fly."""
suite_name = f"{table_name}_suite"
suite = self.context.add_or_update_expectation_suite(expectation_suite_name=suite_name)
# Filter metadata constraints for the specific target asset
table_rules = rules_df[rules_df['table_name'] == table_name]
for _, row in table_rules.iterrows():
# Parse JSON kwargs configuration gracefully
kwargs = row['expectation_kwargs']
if isinstance(kwargs, str):
kwargs = json.loads(kwargs)
kwargs['column'] = row['column_name']
# Programmatically map string values to structured GX expectation objects
expectation_config = gx.core.ExpectationConfiguration(
expectation_type=row['expectation_type'],
kwargs=kwargs,
meta={"notes": f"Automated constraint enforcement for column: {row['column_name']}"}
)
suite.add_expectation(expectation_config)
self.context.add_or_update_expectation_suite(suite=suite)
return suite
def execute_quality_checkpoint(self, table_name: str, target_df: pd.DataFrame):
"""Builds a runtime batch request and evaluates data against the generated suite."""
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
suite_name = f"{table_name}_suite"
checkpoint_name = f"{table_name}_checkpoint"
# Unique runtime composite signature prevents processing trace collisions
batch_request = RuntimeBatchRequest(
datasource_name="lakehouse_runtime_datasource",
data_connector_name="runtime_data_connector",
data_asset_name=f"{table_name}_{timestamp}",
runtime_parameters={"batch_data": target_df},
batch_identifiers={"table_name": table_name, "execution_timestamp": timestamp}
)
# Register and fire a dynamic checkpoint execution
self.context.add_or_update_checkpoint(
name=checkpoint_name,
config_version=1,
class_name="SimpleCheckpoint",
validations=[{
"batch_request": batch_request,
"expectation_suite_name": suite_name
}]
)
logger.info(f"Launching data quality checkpoint for table: {table_name}")
return self.context.run_checkpoint(checkpoint_name=checkpoint_name)
# Production Loop Execution Pattern
if __name__ == "__main__":
SF_CONN = "snowflake://<user>:<pass>@<account>/<db>/<schema>?warehouse=COMPUTE_WH&role=SYSADMIN"
dq_engine = MetadataDataQualityEngine(connection_string=SF_CONN)
metadata_rules = dq_engine.fetch_active_metadata()
distinct_target_tables = metadata_rules['table_name'].unique()
for current_table in distinct_target_tables:
try:
# Stage current batch dataset from target engine
raw_data_df = pd.read_sql(f"SELECT * FROM {current_table}", dq_engine.engine)
# Step 1: Build suite dynamically from relational rules
dq_engine.compile_expectation_suite(table_name=current_table, rules_df=metadata_rules)
# Step 2: Validate batch data and extract metrics payload
eval_result = dq_engine.execute_quality_checkpoint(table_name=current_table, target_df=raw_data_df)
if not eval_result["success"]:
logger.warning(f"Data Quality anomalies detected on asset: {current_table}")
else:
logger.info(f"Asset {current_table} successfully cleared all metadata expectations.")
except Exception as err:
# Fault isolation ensures an asset failure never crashes cascading pipeline steps
logger.error(f"Processing loop interrupted on asset {current_table}: {str(err)}")
continue
3. Production-Grade Engineering Guardrails
Building a dynamic system requires putting structural guardrails around the execution engine to prevent it from failing under enterprise pressures.
- Fault Isolation and Pipeline Resilience: Never let a validation failure on an upstream or non-critical business table halt your entire orchestration loop. Wrapping individual target assets in localized try-except blocks ensures that a failure on a secondary table (like CUSTOMERS) does not block downstream transactional tables (like ORDERS) from completing their validation lifecycles.
- Idempotency and Batching Identifiers: Every unique quality run must be traceable back to a specific moment in time to avoid overwriting or colliding results in your metadata tracking layer. Pair the
table_namewith an immutableexecution_timestamp(such as a UTC ISO string) as a composite batch identifier. This guarantees an explicit audit trail across parallel streaming windows or backfilled data runs. - Metadata-as-Code Frameworks: Treat the validation matrix table with the same operational rigor as production application code. Changes, additions, or deprecations of quality thresholds must follow a strict GitOps progression. Use schema migration version control tools (like Flyway or Liquibase) to manage, track, and deploy DML changes safely across staging and production clusters.
- Proactive Alerting Integration: Local HTML docs are insufficient for zero-downtime platforms. The metadata evaluation output dictionary must be integrated directly into cloud native alerting systems. Configure Webhook integrations or cloud alerting channels to route failed validation metrics directly to Slack channels or PagerDuty schedules. This ensures on-call engineers are proactively notified the moment a metric payload trends outside acceptable operational thresholds.
Summary: The Architectural Impact
Transitioning to a metadata-driven approach shifts data quality from a reactive "clean-up" task to an integrated, proactive engineering asset. By treating validation criteria as configurable metadata parameters rather than hardcoded script directives, architects eliminate technical debt and bridge the gap between business semantics and computing layers.
This decoupled architecture provides the strict governance framework required to support high-stakes analytics and downstream machine learning layers, ensuring that every data element hitting your warehouse is automatically and transparently vetted before reaching production consumers.
Opinions expressed by DZone contributors are their own.
Comments