How to Map EPA MCLs to Relational Database Schemas for Automated Compliance

Translating Safe Drinking Water Act (SDWA) Maximum Contaminant Levels (MCLs) into a production relational database is the specific task of turning a moving regulatory target into stable, queryable rows so that every SCADA reading can be judged against the exact limit in force when it was sampled. Hardcoding thresholds into telemetry tables is the anti-pattern that causes schema drift, breaks historical audits, and turns every rulemaking amendment into a destructive migration. This page is for the municipal Python developers and environmental compliance teams who own that translation layer; it sits under SDWA MCL Reference Mapping and implements the concrete database design that the mapping strategy there describes. The goal is a normalized model that isolates regulatory metadata, temporal validity windows, and sensor readings into distinct structures so compliance calculations are deterministic and audit-ready.

Prerequisites & Environment Setup

This implementation targets PostgreSQL 14+ (for native tsrange exclusion constraints) and Python 3.11+. The compliance resolver uses pandas for vectorized evaluation and SQLAlchemy with the psycopg2 driver for range-based temporal joins. Pin versions so that the QA reconciliation job runs against a reproducible stack:

python3 -m venv .venv
source .venv/bin/activate
pip install "pandas==2.2.2" "SQLAlchemy==2.0.30" "psycopg2-binary==2.9.9" "pytest==8.2.0"

You also need read access to EPA’s official parameter codes so the dimension keys never rely on hand-transcribed values. The regulatory baselines validated below come from the EPA SDWA regulations. Confirm the target schema has a role permitted to create EXCLUDE constraints, which require the btree_gist extension:

CREATE EXTENSION IF NOT EXISTS btree_gist;

Step-by-Step Implementation

MCLs are dynamic: they vary by contaminant, averaging period (single sample, running annual average, or 90th percentile), monitoring frequency, and regulatory revision date. The mapping follows a deterministic pipeline that isolates schema design, identifier standardization, temporal validity, unit conversion, and evaluation. The full model aligns with the shared lineage rules in the Core Architecture & SDWA Compliance Taxonomy, so threshold updates trigger dimension inserts rather than rewrites.

1. Model the dimensional schema

A production model splits regulatory metadata from high-frequency readings across five tables:

  • dim_contaminant: EPA regulatory IDs, CAS numbers, contaminant names, units of measure, and grouping flags.
  • dim_mcl_threshold: MCL values, averaging periods, effective_date, expiration_date, and an is_current flag (Slowly Changing Dimension Type 2).
  • dim_monitoring_point: physical sampling locations, SCADA tag IDs, compliance reporting zones, and jurisdictional boundaries.
  • fact_scada_telemetry: validated concentration readings with UTC timestamps, quality flags, and foreign keys to monitoring points.
  • fact_compliance_events: the derived table storing calculated compliance status, violation codes, aggregation windows, and audit trails.
Dimensional schema mapping EPA MCLs to telemetry and compliance events Entity-relationship diagram. dim_contaminant is the parent dimension keyed on contaminant_id; it has many dim_mcl_threshold rows, one per validity window, and is measured as many fact_scada_telemetry readings. Each telemetry reading is sampled at one dim_monitoring_point, and each fact_compliance_events row is evaluated from one telemetry reading. Regulatory metadata in the dimension tables is isolated from high-frequency readings in the fact tables. has thresholds measured as sampled at evaluated from dim_contaminant dim contaminant_idPK cas_number unit_of_measure dim_mcl_threshold dim contaminant_idFK mcl_value averaging_period validitytsrange is_current fact_scada_telemetry fact contaminant_idFK monitoring_point_idFK timestamp_utc concentration quality_flag dim_monitoring_point dim point_idPK scada_tag_id compliance_zone fact_compliance_events fact compliance_status violation_code aggregation_window
The five-table model: brand-tinted dimensions hold regulatory metadata, neutral fact tables hold high-frequency readings and derived events. Crow's-foot ends read one-to-many — one contaminant has many thresholds and many readings; each reading is sampled at one point and yields one compliance event.

2. Standardize identifiers

Use EPA’s Contaminant_ID as the primary foreign key across every table. Do not key on CAS numbers alone: grouped parameters such as TTHM and HAA5 have no single CASRN value, so a CAS-only join silently drops them.

3. Preserve regulatory history with SCD Type 2

Store MCLs exclusively in dim_mcl_threshold and never overwrite a value. When the EPA revises a rule, expire the old row and insert a new one so historical compliance decisions remain reproducible. A tsrange exclusion constraint guarantees that no two rows for the same contaminant ever claim overlapping validity:

CREATE TABLE dim_mcl_threshold (
    threshold_id     BIGSERIAL PRIMARY KEY,
    contaminant_id   TEXT NOT NULL REFERENCES dim_contaminant(contaminant_id),
    mcl_value        NUMERIC(12, 6),          -- NULL = advisory-only, no enforceable MCL
    averaging_period TEXT NOT NULL,
    unit_of_measure  TEXT NOT NULL,
    validity         TSRANGE NOT NULL,        -- [effective_date, expiration_date)
    is_current       BOOLEAN NOT NULL DEFAULT TRUE,
    EXCLUDE USING gist (
        contaminant_id WITH =,
        validity       WITH &&
    )
);
SCD Type 2 validity windows for one contaminant's MCL A time axis shows two non-overlapping validity windows for the arsenic MCL. Window A carries the older 0.050 mg/L limit and is now expired; a 2006 EPA rule revision closes its expiration_date and opens Window B at the same instant with the current 0.010 mg/L limit. Windows are half-open, so the shared 2006 boundary belongs to Window B. A SCADA reading taken in 2019 drops a vertical line that lands inside exactly one window, Window B, and is judged against 0.010 mg/L. One contaminant · dim_mcl_threshold validity windows Window A · expired MCL 0.050 mg/L · is_current = false Window B · current MCL 0.010 mg/L · is_current = true [ ) [ ) 2006 EPA rule revision expiration_date(A) = effective_date(B) SCADA reading · 2019-06 → Window B 2001 2006 present time
SCD Type 2 in one picture: a revision never overwrites a limit, it closes the old window and opens a new one at the same instant. Half-open ranges keep the 2006 boundary in exactly one window, so every reading resolves to a single enforceable MCL.

4. Bind the averaging period, not just the number

An MCL is a number plus an averaging rule. The reference row declares which rule applies and the evaluator dispatches on it. For running-annual-average contaminants such as total trihalomethanes, the engine must reproduce the quarterly mean exactly:

RAAq=14i=q3qCˉi\text{RAA}_{q} = \frac{1}{4}\sum_{i=q-3}^{q} \bar{C}_{i}

where Cˉi\bar{C}_{i} is the arithmetic mean of the valid samples in quarter ii and qq is the current quarter. Whether each Cˉi\bar{C}_{i} is even complete depends on the statutory cadence owned by the Monitoring Frequency Scheduling module, so the reference table and the scheduler share the same contaminant keys.

5. Normalize units before evaluation

Convert every reading to the EPA-mandated unit of measure (mg/L, µg/L, NTU) before comparison. Store conversion factors in a dedicated dim_unit_conversion table rather than embedding them in code, and enforce a temporal join so each reading resolves the threshold active at its own timestamp:

import logging
from datetime import datetime, timezone

import pandas as pd
from sqlalchemy import text

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def resolve_compliance_status(engine, telemetry_batch_id: str):
    """
    Join SCADA telemetry with the MCL threshold active at each reading's
    timestamp, calculate compliance, and route anomalies to fallback handlers.
    """
    query = """
    SELECT
        t.timestamp_utc,
        t.monitoring_point_id,
        t.contaminant_id,
        t.concentration_raw,
        t.unit_of_measure,
        m.mcl_value,
        m.averaging_period,
        m.validity
    FROM fact_scada_telemetry t
    JOIN dim_mcl_threshold m
      ON t.contaminant_id = m.contaminant_id
     AND m.validity @> t.timestamp_utc
    WHERE t.batch_id = :batch_id
    """
    try:
        with engine.connect() as conn:
            df = pd.read_sql(text(query), conn, params={"batch_id": telemetry_batch_id})
    except Exception as exc:
        logger.error("Threshold resolution failed for batch %s: %s", telemetry_batch_id, exc)
        return route_fallback(telemetry_batch_id, error_type="DB_JOIN_FAILURE")

    if df.empty:
        logger.warning("No active thresholds matched batch. Routing to manual review.")
        return route_fallback(telemetry_batch_id, error_type="NO_ACTIVE_MCL")

    if df["unit_of_measure"].nunique() > 1:
        logger.warning("Unit mismatch detected. Applying standard conversion matrix.")
        df = _apply_unit_conversion(df)

    # Readings with no enforceable MCL are flagged for explicit manual review
    # rather than silently passing.
    df["is_compliant"] = df.apply(
        lambda row: (row["concentration_raw"] <= row["mcl_value"])
        if pd.notnull(row["mcl_value"]) else False,
        axis=1,
    )
    df["compliance_timestamp"] = datetime.now(timezone.utc)
    df["violation_code"] = df["is_compliant"].map({True: "COMPLIANT", False: "EXCEEDANCE"})

    try:
        df.to_sql("fact_compliance_events", engine, if_exists="append", index=False)
        logger.info("Compliance events persisted for batch %s", telemetry_batch_id)
    except Exception as exc:
        logger.error("Failed to write compliance events: %s", exc)
        return route_fallback(telemetry_batch_id, error_type="WRITE_FAILURE")

    return df


def route_fallback(batch_id: str, error_type: str):
    """Immediate operational resolution for pipeline failures."""
    logger.critical("Routing batch %s to quarantine due to %s", batch_id, error_type)
    # Production: INSERT INTO fact_compliance_quarantine and trigger an
    # on-call webhook for the SCADA admin or compliance officer.
    return {"batch_id": batch_id, "error_type": error_type, "status": "QUARANTINED"}


def _apply_unit_conversion(df: pd.DataFrame) -> pd.DataFrame:
    """Join dim_unit_conversion and rescale concentration_raw. Stubbed here."""
    return df
Compliance resolver flow with fallback routing A top-to-bottom decision flow for resolve_compliance_status. A temporal join pairs telemetry with active thresholds. If the join fails the batch routes to route_fallback DB_JOIN_FAILURE. If no active MCL matched it routes to NO_ACTIVE_MCL. If units are inconsistent a conversion step runs before evaluation. The resolver then computes is_compliant and violation_code, and writes fact_compliance_events; a failed write routes to WRITE_FAILURE. All three fallbacks quarantine the batch, while a successful write persists the compliance events. Yes No Yes No Yes No Yes No Temporal join telemetry × active thresholds DB join OK? Active MCL matched? Units consistent? Apply unit conversion Compute is_compliant + violation_code Event write OK? fact_compliance_events persisted route_fallback → QUARANTINED route_fallback DB_JOIN_FAILURE route_fallback NO_ACTIVE_MCL route_fallback WRITE_FAILURE
The resolver's decision flow: three guarded gates on the main path each divert to a dashed quarantine box rather than a silent pass, and only a clean run reaches persisted compliance events.

The structured violation_code this resolver emits is the exact input consumed downstream by Translating EPA Violation Codes to Internal Alerts, which maps each code onto operator-facing notifications.

Configuration Reference

The tables below define the core parameters an implementer sets once and the enumerated codes the resolver emits. Keep the averaging-period and status enums in a shared module so the database CHECK constraints and the Python evaluator never drift apart.

Parameter Table Type Purpose
contaminant_id dim_contaminant TEXT EPA regulatory ID; primary join key across all tables
mcl_value dim_mcl_threshold NUMERIC(12,6) Enforceable limit; NULL marks an advisory-only parameter
averaging_period dim_mcl_threshold TEXT enum Selects the evaluation formula (see below)
validity dim_mcl_threshold TSRANGE [effective_date, expiration_date) window for temporal joins
unit_of_measure dim_contaminant TEXT EPA-mandated unit; readings normalized to this before comparison
scada_tag_id dim_monitoring_point TEXT Links a physical sensor tag to a compliance reporting zone
Averaging period code Meaning Compared value
SINGLE_SAMPLE Acute limit (e.g. nitrate) Individual result
RAA Running annual average (TTHM, HAA5) Mean of four quarterly means
LRAA Locational running annual average Per-location running annual average
PERCENTILE_90 90th-percentile action level (e.g. lead) 90th percentile of site samples
Status / error code Emitted by Meaning
COMPLIANT resolver Reading at or below the active MCL
EXCEEDANCE resolver Reading above the active MCL, or no enforceable limit found
DB_JOIN_FAILURE fallback Temporal join raised; batch quarantined
NO_ACTIVE_MCL fallback No threshold window covered the reading timestamp
WRITE_FAILURE fallback Event persistence failed; batch quarantined for retry

Verification & Testing

Prove the temporal join resolves the correct threshold across a rule revision before wiring the resolver into production. The test below seeds two non-overlapping validity windows and asserts a reading is judged against the window active on its own date:

from datetime import datetime, timezone

import pandas as pd
from sqlalchemy import create_engine, text


def test_temporal_join_selects_active_threshold(tmp_path):
    engine = create_engine(f"sqlite:///{tmp_path}/compliance.db")
    with engine.begin() as conn:
        conn.execute(text(
            "CREATE TABLE dim_mcl_threshold "
            "(contaminant_id TEXT, mcl_value REAL, effective TEXT, expiration TEXT)"
        ))
        conn.execute(text(
            "INSERT INTO dim_mcl_threshold VALUES "
            "('1005', 0.010, '2006-01-01', '2024-01-01'), "  # older arsenic MCL era
            "('1005', 0.010, '2024-01-01', '2999-01-01')"
        ))

    reading_ts = datetime(2024, 6, 1, tzinfo=timezone.utc).isoformat()
    query = text(
        "SELECT mcl_value FROM dim_mcl_threshold "
        "WHERE contaminant_id = '1005' AND :ts >= effective AND :ts < expiration"
    )
    with engine.connect() as conn:
        rows = pd.read_sql(query, conn, params={"ts": reading_ts})

    assert len(rows) == 1              # exactly one window matches
    assert rows.iloc[0]["mcl_value"] == 0.010

Acceptance criteria for the mapping layer:

Troubleshooting & Gotchas

  • Threshold expiration gaps. When a new EPA rule supersedes an older one, a temporal gap can open between the old expiration_date and the new effective_date, and readings in that window match no row. Diagnosis: NO_ACTIVE_MCL spikes for a single contaminant on a known revision date. Fix: default to the most recent valid threshold and flag the record REGULATORY_GAP_PENDING_REVIEW rather than dropping it.
  • SCADA tag drift. Telemetry tags remap to different physical locations after PLC upgrades or sensor swaps, so a valid reading joins to the wrong compliance zone. Fix: keep a dim_tag_mapping_history table with its own validity windows and route unmatched tags to fact_orphan_readings with a GIS/SCADA alert. The same UTC-alignment discipline in Aligning Irregular SCADA Timestamps to UTC prevents drift-induced join misses.
  • Missing MCL values. Some contaminants carry a health advisory but no enforceable MCL. Comparing against a NULL limit silently passes every reading. Fix: treat mcl_value IS NULL as a skip-and-log path (ADVISORY_ONLY_PARAMETER), which the code above does by defaulting is_compliant to False and routing for review.
  • Local timezone leakage. Storing effective_date as a naive local date makes a BETWEEN join off by hours near midnight and by a full hour across DST. Fix: store all bounds in UTC and use half-open tsrange (@>), never inclusive BETWEEN on both ends, so a boundary timestamp resolves to exactly one window.
  • Overlapping validity windows. Without the exclusion constraint, a bad backfill can leave two active rows and the join fans out, doubling fact_compliance_events. Fix: enforce the EXCLUDE USING gist constraint at the schema level so the bad insert fails loudly instead of corrupting the audit trail. Downstream, the real-time MCL exceedance logic assumes exactly one threshold per reading.

By decoupling regulatory metadata from high-frequency telemetry, utilities gain deterministic compliance calculations, audit-ready reporting, and pipelines that absorb EPA rulemaking through new dimension rows rather than schema refactoring.