Stop silent schema drift before it breaks your data pipelines
Project description
๐ก๏ธ Schema Guard
Stop silent schema drift before it breaks your production data pipelines.
Schema Guard is a lightweight CLI tool that captures database schema snapshots and acts as a CI/CD gate, blocking deployments when unauthorized schema changes are detected. It's the missing guardrail for data engineers who've been burned by unexpected column drops, type changes, or nullability shifts.
Why Schema Guard?
Every data engineer knows the 2 AM nightmare:
- A source team adds a column, changes a type, or drops a
NOT NULLwithout notice. - Your ETL pipeline doesn't failโit silently writes
NULLs or corrupts downstream data. - Executive dashboards break, and you spend hours tracing the issue back to a trivial schema change.
Schema Guard stops this at the CI gate. You define a data contract (YAML), capture a trusted schema snapshot, and then in your deployment pipeline, schema-guard gate compares the live source against the snapshot and contract. Any drift fails the build before it hits production.
๐ง Architecture Overview
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Contract โโโโโโโโถ โ CLI (snap) โโโโโโโโถโ Snapshot JSON โ
โ (orders.yaml) โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ โ โ
โ Live Database โ โ
โโโโโโโโโโโโโโโโโโโ โ
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ CLI (gate) โ
โ Compares live schema โ
โ vs snapshot + contractโ
โโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ Diff Engine โ
โ Detects violations โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโผโโโโโโโโโโโโ
โ Alerter (email) โ
โ Sends notifications โ
โโโโโโโโโโโโโโโโโโโโโโโโโ
- Contract โ Describes the expected schema (columns, types, nullability, allowed drifts).
- Snapshot โ A frozen, integrity-verified JSON representation of the real schema at a trusted point in time.
- Gate โ Compares live schema to snapshot + contract. If drift is detected, it logs violations, sends an email, and exits with code 1 to fail the CI pipeline.
โจ Key Features
- Snapshot integrity verification โ Every snapshot is SHA-256 hashed. Tampered or corrupted snapshot files are detected and rejected on load.
- Overwrite protection โ The
snapcommand refuses to overwrite an existing baseline unless--forceis passed, preventing accidental baseline drift. - Case-insensitive type comparison โ
INTEGERvsintegerandNUMERIC(10, 2)vsnumeric(10,2)are treated as equal. No more false positives. - Primary key drift detection โ Dropped or added primary key constraints are flagged as CRITICAL violations.
- Contract validation โ Malformed YAML contracts fail fast with clear error messages instead of cryptic
KeyErrorcrashes. - Contract-vs-snapshot warnings โ If the snapshot itself doesn't match the contract expectations, you'll see a WARNING notice so you know the baseline may have been captured at a bad time.
- Allowed drift visibility โ When an
allowed_driftrule matches, it's logged as an INFO notice instead of being silently swallowed. - Reliable alerting โ Email alert failures are reported to stderr. Use
--require-alertto fail the gate if email delivery fails.
๐ Installation
From source (for development)
git clone https://github.com/anilsolanki2645/schema-guard/
cd schema-guard
pip install -e ".[dev]"
From PyPI (once published)
pip install db-schema-guard
Requires Python โฅ 3.8. Dependencies are installed automatically.
โก Quick Start
1. Create the database table
CREATE TABLE public.orders (
order_id INT PRIMARY KEY,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL
);
INSERT INTO orders VALUES (1, 99.99, 'shipped');
2. Define a contract โ contracts/orders.yaml
source:
name: prod_orders
type: postgres
connection: "env:DB_CONNECTION_STRING" # see Configuration
schema: public
table: orders
columns:
- name: order_id
type: integer
nullable: false
- name: amount
type: numeric(10,2)
nullable: false
allowed_drift:
- from: "numeric(10,2)"
to: "numeric(12,2)"
- name: status
type: character varying(20)
nullable: false
3. Set up environment variables
Copy the example file and fill in your values:
cp .env.example .env
Edit .env:
DB_CONNECTION_STRING=postgresql://postgres:yourpassword@localhost:5432/schema_guard
Note: Special characters must be URLโencoded, e.g.,
@โ%40
โ ๏ธ Security: Never commit
.envto version control. The.gitignoreis already configured to exclude it. Use.env.exampleas a template reference.
4. Capture a snapshot of the current schema
schema-guard snap --contract contracts/orders.yaml --snapshot-file snapshots/orders.json
โ
Snapshot saved to snapshots/orders.json
5. Verify the gate passes
schema-guard gate --contract contracts/orders.yaml --snapshot-file snapshots/orders.json
โ Schema matches snapshot. No drift.
6. Simulate a drift
ALTER TABLE public.orders ALTER COLUMN amount DROP NOT NULL;
7. Run the gate again (should fail)
schema-guard gate --contract contracts/orders.yaml --snapshot-file snapshots/orders.json
โ Schema drift detected:
- CRITICAL: Column 'amount' nullable changed from False to True.
Command exits with code 1, and an email alert is sent (if configured).
8. Revert to clean state
ALTER TABLE public.orders ALTER COLUMN amount SET NOT NULL;
Now the gate passes again.
๐ Configuration
Database connection
Define a PostgreSQL connection string in .env:
DB_CONNECTION_STRING=postgresql://user:password@host:5432/dbname
In the contract, reference it with "env:DB_CONNECTION_STRING".
The tool's contract.py resolves any value beginning with env: to the corresponding environment variable.
Email alerting (optional)
Add these to .env to receive drift alerts via SMTP:
EMAIL_ENABLED=true
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your_email@gmail.com
EMAIL_PASSWORD=your_app_password # Use an App Password, not your real password
EMAIL_FROM=your_email@gmail.com
EMAIL_TO=oncall@example.com
EMAIL_SUBJECT=Schema Drift Detected
If EMAIL_ENABLED is not true, alerts are silently skippedโthe gate still works, but no email is sent.
๐ Command Reference
snap โ capture a schema snapshot
schema-guard snap --contract <contract.yaml> --snapshot-file <snapshot.json> [--force]
| Flag | Description |
|---|---|
--contract |
(required) Path to the contract YAML file |
--snapshot-file |
Where to save the snapshot (default: schema_snapshot.json) |
--force |
Overwrite an existing snapshot. Without this flag, the command will refuse to overwrite and show a diff of detected changes |
- Connects to the source defined in the contract.
- Inspects the table and saves column metadata (name, type, nullable, primary key) along with a SHA-256 hash and timestamp.
- Overwrite protection: If a snapshot already exists, you must pass
--forceto replace it. This prevents accidentally re-baselining against a drifted schema.
gate โ check for drift
schema-guard gate --contract <contract.yaml> --snapshot-file <snapshot.json> [--require-alert]
| Flag | Description |
|---|---|
--contract |
(required) Path to the contract YAML file |
--snapshot-file |
Baseline snapshot to compare against (default: schema_snapshot.json) |
--require-alert |
Exit with code 2 if email alert fails to send |
- Verifies snapshot integrity (SHA-256 hash check) before comparing.
- Extracts the current live schema.
- Validates contract expectations against the snapshot (emits warnings if mismatched).
- Compares live schema against the snapshot using the contract rules.
- Violation types:
| Violation | Severity |
|---|---|
| Column removed | CRITICAL |
Type changed (not in allowed_drift) |
CRITICAL |
| Nullable changed | CRITICAL |
| Primary key constraint dropped | CRITICAL |
| Primary key constraint added | CRITICAL |
| New column added | WARNING |
| Allowed drift matched | INFO (notice, non-blocking) |
Exit codes
| Code | Meaning |
|---|---|
0 |
Gate passed โ no drift detected |
1 |
Schema drift detected โ violations found |
2 |
Email alert failed (when --require-alert is set) or unsupported source type |
3 |
Snapshot integrity check failed (file was tampered with or corrupted) |
๐งฑ Contract YAML Specification
A contract file defines the expected state of a data source. The contract is validated on load โ missing or malformed fields produce clear error messages.
source:
name: friendly_name # Used in logs (optional)
type: postgres # Source type (currently only postgres supported)
connection: "env:DB_CONNECTION_STRING" # Database URL or env:VARIABLE
schema: public # Database schema (required)
table: orders # Table name (required)
columns: # Optional section
- name: order_id # Column name (required)
type: integer # Expected type (required, case-insensitive matching)
nullable: false # Expected nullability (required)
allowed_drift: # Optional โ type changes allowed without alert
- from: "numeric(10,2)"
to: "numeric(12,2)"
Required fields
| Section | Required Keys |
|---|---|
source |
type, connection, schema, table |
Each column in columns |
name, type, nullable |
Each rule in allowed_drift |
from, to |
Matching behavior
typecomparison is case-insensitive and whitespace-insensitive.INTEGERmatchesinteger, andNUMERIC(10, 2)matchesnumeric(10,2).nullablemust betrueorfalse.allowed_driftrules are also matched case-insensitively. When a rule matches, an INFO notice is logged for visibility.
๐ Alerting (Email)
The alerter.py module sends an email with the list of violations. Uses Python's smtplib and email libraries (no extra deps).
- Alert errors are printed to stderr as
[alerter] Failed to send email: ... - The gate still fails with exit code 1 regardless of email success/failure.
- To require successful email delivery, use
--require-alert. If the email fails, the gate exits with code 2.
โ๏ธ CI/CD Integration
GitHub Actions example โ .github/workflows/schema-check.yml
name: Schema Drift Gate
on: [push, pull_request]
jobs:
schema-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install schema-guard
run: pip install -e . # or pip install db-schema-guard
- name: Run schema gate
run: schema-guard gate --contract contracts/orders.yaml --snapshot-file snapshots/orders.json --require-alert
env:
DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }}
EMAIL_ENABLED: true
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
EMAIL_PORT: ${{ secrets.EMAIL_PORT }}
EMAIL_USER: ${{ secrets.EMAIL_USER }}
EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
EMAIL_TO: ${{ secrets.EMAIL_TO }}
Store these values as GitHub Secrets.
Now every push and pull request will be checked for schema drift automatically.
๐งช Testing
Schema Guard includes a comprehensive test suite with 39 unit tests covering the core modules. No database connection is required to run the tests โ they use in-memory fixtures.
Running the test suite
# Install with dev dependencies (includes pytest)
pip install -e ".[dev]"
# Run all tests with verbose output
python -m pytest tests/ -v
Expected output:
tests/test_contract.py::TestValidContract::test_loads_valid_contract PASSED
tests/test_contract.py::TestValidContract::test_contract_without_columns_section PASSED
tests/test_contract.py::TestEnvResolution::test_resolves_env_variable PASSED
tests/test_contract.py::TestEnvResolution::test_missing_env_variable_raises PASSED
tests/test_contract.py::TestContractValidation::test_missing_source_section PASSED
tests/test_contract.py::TestContractValidation::test_missing_source_type PASSED
tests/test_contract.py::TestContractValidation::test_missing_source_connection PASSED
tests/test_contract.py::TestContractValidation::test_unsupported_source_type PASSED
tests/test_contract.py::TestContractValidation::test_column_missing_name PASSED
tests/test_contract.py::TestContractValidation::test_column_missing_type PASSED
tests/test_contract.py::TestContractValidation::test_malformed_allowed_drift PASSED
tests/test_contract.py::TestContractValidation::test_empty_yaml_raises PASSED
tests/test_diff_engine.py::TestNormalizeType::test_lowercase PASSED
tests/test_diff_engine.py::TestNormalizeType::test_strip_spaces PASSED
tests/test_diff_engine.py::TestNormalizeType::test_already_normalized PASSED
tests/test_diff_engine.py::TestColumnRemoved::test_detects_removed_column PASSED
tests/test_diff_engine.py::TestColumnAdded::test_detects_added_column PASSED
tests/test_diff_engine.py::TestNullableChange::test_detects_nullable_change PASSED
tests/test_diff_engine.py::TestNullableChange::test_no_violation_when_nullable_matches PASSED
tests/test_diff_engine.py::TestTypeChange::test_detects_type_change PASSED
tests/test_diff_engine.py::TestTypeChange::test_case_insensitive_no_false_positive PASSED
tests/test_diff_engine.py::TestTypeChange::test_whitespace_insensitive PASSED
tests/test_diff_engine.py::TestAllowedDrift::test_allowed_drift_passes PASSED
tests/test_diff_engine.py::TestAllowedDrift::test_disallowed_drift_fails PASSED
tests/test_diff_engine.py::TestAllowedDrift::test_allowed_drift_case_insensitive PASSED
tests/test_diff_engine.py::TestPrimaryKeyChange::test_detects_pk_dropped PASSED
tests/test_diff_engine.py::TestPrimaryKeyChange::test_detects_pk_added PASSED
tests/test_diff_engine.py::TestPrimaryKeyChange::test_no_violation_when_pk_matches PASSED
tests/test_diff_engine.py::TestContractVsSnapshot::test_warns_type_mismatch PASSED
tests/test_diff_engine.py::TestContractVsSnapshot::test_warns_nullable_mismatch PASSED
tests/test_diff_engine.py::TestContractVsSnapshot::test_no_warning_when_contract_matches PASSED
tests/test_diff_engine.py::TestNoDrift::test_identical_schemas_pass PASSED
tests/test_snapshot.py::TestRoundTrip::test_save_and_load PASSED
tests/test_snapshot.py::TestRoundTrip::test_hash_is_deterministic PASSED
tests/test_snapshot.py::TestRoundTrip::test_creates_parent_directories PASSED
tests/test_snapshot.py::TestIntegrityVerification::test_tampered_schema_raises PASSED
tests/test_snapshot.py::TestIntegrityVerification::test_tampered_hash_raises PASSED
tests/test_snapshot.py::TestIntegrityVerification::test_missing_hash_raises PASSED
tests/test_snapshot.py::TestIntegrityVerification::test_valid_snapshot_passes PASSED
============================= 39 passed ==============================
What the tests cover
| Test File | Module | What's Tested |
|---|---|---|
test_diff_engine.py |
diff_engine.py |
Column added/removed, type change detection, case-insensitive comparison, nullable change, primary key drift, allowed drift (with logging), contract-vs-snapshot validation, no-drift happy path |
test_contract.py |
contract.py |
Valid YAML loading, env variable resolution, missing env var error, missing required keys (source, type, connection, etc.), unsupported source type, malformed allowed_drift, empty YAML |
test_snapshot.py |
snapshot.py |
Save/load round-trip, SHA-256 hash determinism, parent directory auto-creation, tamper detection (modified schema, fake hash, missing hash field) |
Running a specific test file
python -m pytest tests/test_diff_engine.py -v # Only diff engine tests
python -m pytest tests/test_contract.py -v # Only contract tests
python -m pytest tests/test_snapshot.py -v # Only snapshot tests
Running a specific test class or method
python -m pytest tests/test_diff_engine.py::TestPrimaryKeyChange -v # One class
python -m pytest tests/test_snapshot.py::TestIntegrityVerification::test_tampered_schema_raises -v # One test
๐ Project Structure
schema-guard/
โโโ .github/workflows/
โ โโโ schema-check.yml # CI/CD pipeline config
โโโ contracts/
โ โโโ orders.yaml # Your data contracts
โโโ snapshots/ # Baseline snapshots (autoโcreated)
โ โโโ orders.json
โโโ src/schema_guard/
โ โโโ __init__.py
โ โโโ cli.py # Click CLI (snap & gate commands)
โ โโโ contract.py # YAML parser, env resolver & validator
โ โโโ extractors/
โ โ โโโ __init__.py
โ โ โโโ postgres.py # PostgreSQL schema inspector
โ โโโ snapshot.py # Save/load snapshot JSON with integrity checks
โ โโโ diff_engine.py # Drift detection logic (type, nullable, PK)
โ โโโ alerter.py # SMTP email alerting
โโโ tests/
โ โโโ test_diff_engine.py # 20 tests โ drift detection logic
โ โโโ test_contract.py # 12 tests โ YAML loading & validation
โ โโโ test_snapshot.py # 7 tests โ save/load & integrity
โโโ pyproject.toml # Build & packaging config
โโโ .env.example # Template for environment variables
โโโ .gitignore
โโโ README.md
๐ Extending with New Extractors
To support another data source (Snowflake, BigQuery, S3 Parquet, etc.):
- Create a new file in
src/schema_guard/extractors/(e.g.,snowflake.py). - Implement a function
get_schema(connection_string, schema_name, table_name)returning:
{
"table": "schema.table",
"columns": [
{
"name": "col1",
"type": "varchar",
"nullable": True,
"primary_key": False
},
...
]
}
- In
cli.py, add anelifbranch for the new source type. - Add any required packages to
pyproject.toml.
Pull requests are welcome!
๐งฉ Troubleshooting
| Problem | Solution |
|---|---|
FileNotFoundError: snapshots/orders.json |
Run schema-guard snap first to create the baseline snapshot. The snapshots/ directory is autoโcreated. |
Snapshot integrity check FAILED |
The snapshot file was manually edited or corrupted. Reโcapture a trusted snapshot with schema-guard snap --force. |
Contract 'source' is missing required key |
Your contract YAML is missing a required field. Check the Contract YAML Specification. |
Unsupported source type |
Only postgres is currently supported. Check for typos in source.type. |
Could not parse SQLAlchemy URL |
The connection string is invalid. Check .env and contract; ensure env: placeholders resolve correctly. |
FATAL: password authentication failed |
Wrong credentials or special characters not URLโencoded. Encode @ as %40, % as %25, etc. Test with psql. |
| Email not sent | Set EMAIL_ENABLED=true. Use an App Password for Gmail. Look for [alerter] messages in stderr. |
| Gate passes when it shouldn't | Ensure the snapshot file is the correct baseline and hasn't been tampered with. The hash check should catch this. |
ModuleNotFoundError when running CLI |
Run pip install -e . from the project root. |
| Snapshot overwrite refused | Pass --force to schema-guard snap to overwrite an existing snapshot. |
๐ Security Best Practices
- Never commit
.envโ Use.env.exampleas a template. The.gitignoreexcludes.envby default. - Use GitHub Secrets for CI/CD โ Store
DB_CONNECTION_STRING, email credentials, etc. as encrypted secrets. - Rotate credentials if they were ever exposed in version control history.
- Snapshot integrity โ Every snapshot includes a SHA-256 hash. If the file is tampered with, the
gatecommand will reject it immediately.
๐ License
Schema Guard is openโsource under the MIT License. See the LICENSE file for details.
Built with frustration turned into code by a data engineer who just wants to sleep through the night. โจ
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file db_schema_guard-0.1.1.tar.gz.
File metadata
- Download URL: db_schema_guard-0.1.1.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
627b6a430e097a1087ce3cf79bfedb9d59c50f91fc9051321f6fb7a4ba13f4e0
|
|
| MD5 |
bea7b033b44e8738b97ff1a653d6f7c4
|
|
| BLAKE2b-256 |
820596fcd0e2c74a81a537ae8d6dd1be564a5f308bcb6fedabfd9211e2c38675
|
File details
Details for the file db_schema_guard-0.1.1-py3-none-any.whl.
File metadata
- Download URL: db_schema_guard-0.1.1-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3918dc3e12ffb2b694c2a2ca6585bee7677d4d5dc0307f2f4d2d4a37026404d
|
|
| MD5 |
07b2c11f2ca77e632876950566d446d5
|
|
| BLAKE2b-256 |
8b90c8cbccd004e9147a69f4ea2fecea0e5d97990b8ee59dcf76c14ce3de90bc
|