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
- Multi-Database Support โ Supports schema drift detection for PostgreSQL, MySQL, Snowflake, SQL Server, Oracle, and Databricks.
- Automatic Contract Generation โ Generate fully specified contract YAML files directly from live database tables using the new
generatecommand. - YAML Configuration File โ Centralized configuration file support (
schema-guard-config.yml) with per-environment profiles (dev, staging, prod) and Jinja2 templating. - Dry-Run Gate Mode โ Preview drift violations with
--dry-runto test CI/CD pipelines without failing the build or sending email notifications. - Verbose Debug Logging โ Step-by-step logging with
--verbose/-vdetailing connection pooling, schema extraction, and comparison checks. - Change History & Audit Trail โ Persist gate execution results (timestamp, profile, violations, notices) in a dedicated database table for compliance.
- Contract Integrity Verification โ Lock your YAML contract files by creating a
.locksidecar with a SHA-256 signature, preventing unauthorized contract edits. - Snapshot Integrity Check โ Baseline snapshots are SHA-256 hashed. Tampered or corrupted snapshot files are automatically blocked.
- Overwrite Protection โ The
snapcommand prevents accidentally overwriting your baseline snapshot unless--forceis specified. - Case-Insensitive Comparison โ Treats type mismatches like
INTEGERvsintegeror whitespace shifts as identical to reduce false alarms. - Primary Key & Nullability Gate โ Tracks additions/deletions of PK constraints and nullability shifts as CRITICAL violations.
- Allowed Drift Rules โ Map minor expected type changes (e.g.
numeric(10,2)tonumeric(12,2)) in the contract without breaking the pipeline.
๐ 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 database 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] [--verbose] [--config <config.yml>]
| Flag / Option | Description |
|---|---|
--contract |
Path to the contract YAML file (required if not defined in config) |
--snapshot-file |
Where to save the snapshot (default: schema_snapshot.json) |
--force |
Overwrite an existing snapshot and display a diff of changes |
--verbose, -v |
Enable verbose step-by-step debug logging |
--config, -c |
Path to schema-guard configuration YAML file (auto-detects schema-guard-config.yml in cwd) |
- Connects to the source database and extracts columns, types, nullability, and primary key constraints.
- Saves the schema details with a SHA-256 integrity hash.
- Overwrite protection guarantees you do not accidentally overwrite a known good baseline without
--force.
gate โ verify schema against snapshot
schema-guard gate [--contract <contract.yaml>] [--snapshot-file <snapshot.json>] [--require-alert] [--dry-run] [--verbose] [--config <config.yml>]
| Flag / Option | Description |
|---|---|
--contract |
Path to the contract YAML file (required if not defined in config) |
--snapshot-file |
Baseline snapshot to compare against (default: schema_snapshot.json) |
--require-alert |
Exit with code 2 if email alert fails to send |
--dry-run |
Preview drift results without failing CI or sending alerts (exit code is always 0) |
--verbose, -v |
Enable verbose step-by-step comparison logging |
--config, -c |
Path to schema-guard configuration YAML file (auto-detects schema-guard-config.yml in cwd) |
- Verifies snapshot file integrity.
- If a contract
.locksidecar exists, verifies that the contract hasn't been modified. - Compares live schema metadata against the baseline snapshot using contract rules.
- If history connection parameters are configured, records the PASS/FAIL execution to the change history DB table.
lock โ lock a contract file
schema-guard lock --contract <contract.yaml> [--verbose]
- Computes a SHA-256 hash of the contract YAML file and writes a
<contract.yaml>.locksidecar. - If the lock file exists,
gatewill verify that the contract has not been altered before checking drift, guaranteeing contract integrity. - Re-run this command after making intentional edits to the contract to update the signature.
generate โ automatically generate a contract file
schema-guard generate [--type <database_type>] [--connection <conn>] [--schema <schema>] [--table <table>] [--output <output_path>] [--force] [--verbose]
| Flag / Option | Description |
|---|---|
--type, -t |
Database platform (e.g. postgres, mysql, snowflake, sqlserver, oracle, databricks). Optional; prompts interactively if omitted. |
--connection, --conn |
Connection URL, JSON connection dictionary, or env variable reference. Optional; prompts interactively if omitted. |
--schema, -s |
Target schema name. Optional; prompts with a database-specific smart default (e.g. public, dbo, default). |
--table, --tbl |
Target table name. Optional; prompts interactively if omitted. |
--output, -o |
Output file path for the contract YAML. Optional; defaults to contracts/<table_name>.yaml. |
--force, -f |
Overwrite the contract file if it already exists. |
--verbose, -v |
Enable verbose step-by-step debug logging. |
- Interactive Mode: Simply run
schema-guard generateto launch an interactive terminal guide that asks for parameters with dropdown choices and smart defaults. - Connects to the database and extracts column names, data types, and nullability.
- Formats the resulting metadata into a valid Schema Guard contract YAML.
- Overwrite protection checks if
--outputalready exists. In interactive mode, it will prompt for confirmation; in scripts, it will refuse unless--forceis provided. - Credential security: If an env variable reference is passed (e.g.
--connection env:MY_CONN), the resolver fetches the secret to extract the schema, but writes the original"env:MY_CONN"literal in the contract file. - Driver hints: If a database driver is missing, the command catches the error and prints a friendly
pip installhint with the correct package extra.
history โ view execution history
schema-guard history [--limit <count>] [--verbose] [--config <config.yml>]
- Connects to the database and fetches the recent gate check execution history logs.
- Displays timestamp, result status (PASS/FAIL), profile, contract path, and violations counts.
- Run with
--verboseto view details on specific violations recorded in past failing runs.
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 database/extractor error |
3 |
Snapshot integrity check failed (file was tampered with or corrupted) |
4 |
Contract integrity check failed (YAML modified since it was locked) |
๐งฑ 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 (postgres, mysql, snowflake, sqlserver, oracle, databricks)
connection: "env:DB_CONNECTION_STRING" # Database URL or env:VARIABLE or Connection Dictionary
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)"
Connection Dictionary (Optional)
Instead of a single URL string, the connection key under source can be defined as a mapping of parameters (ideal for config-managed DB setups like Snowflake and Databricks):
source:
type: snowflake
schema: PUBLIC
table: ORDERS
connection:
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
database: PROD_DB
warehouse: COMPUTE_WH
role: DEV_ROLE
Databricks supports access tokens, username/password, and OAuth 2.0 Client Credentials:
source:
type: databricks
schema: default
table: customers
connection:
auth_type: oauth
server_hostname: "{{ env_var('DATABRICKS_HOST') }}"
http_path: "{{ env_var('DATABRICKS_PATH') }}"
client_id: "{{ env_var('DATABRICKS_CLIENT_ID') }}"
client_secret: "{{ env_var('DATABRICKS_CLIENT_SECRET') }}"
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 61 unit tests covering the core modules. No database connection is required to run the tests โ they use in-memory fixtures and mock inspection suites.
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
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) |
test_new_features.py |
config.py, logger.py, contract.py (integrity) |
YAML configuration file loading, per-environment profile loading & merging, default configuration merging, email environment injection, contract hashing/locking, and logging verbosity settings |
Running specific tests
python -m pytest tests/test_new_features.py -v # Only new features tests
python -m pytest tests/test_diff_engine.py -v # Only diff engine tests
๐ Project Structure
schema-guard/
โโโ contracts/
โ โโโ orders.yaml # Your data contracts
โโโ snapshots/ # Baseline snapshots (autoโcreated)
โ โโโ orders.json
โโโ src/schema_guard/
โ โโโ __init__.py
โ โโโ cli.py # Click CLI commands (snap, gate, lock, history)
โ โโโ config.py # YAML config file loader and profile merger
โ โโโ logger.py # Verbose structured logger configuration
โ โโโ contract.py # YAML parser, env resolver & integrity checks
โ โโโ history.py # Change history audit trail DB recorder
โ โโโ extractors/
โ โ โโโ __init__.py
โ โ โโโ base.py # Base extractor interface & URL builder
โ โ โโโ postgres.py # PostgreSQL extractor
โ โ โโโ mysql.py # MySQL extractor
โ โ โโโ snowflake.py # Snowflake extractor
โ โ โโโ sqlserver.py # SQL Server extractor
โ โ โโโ oracle.py # Oracle extractor
โ โ โโโ databricks.py # Databricks extractor
โ โโโ 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 # Drift engine tests
โ โโโ test_contract.py # Contract loader & validation tests
โ โโโ test_snapshot.py # Snapshot integrity tests
โ โโโ test_new_features.py # Config, logging, and lock verification tests
โโโ pyproject.toml # Build config & DB extras definition
โโโ Dockerfile # Multi-stage lightweight CI Docker image
โโโ .dockerignore # Docker build context filters
โโโ schema-guard-config.example.yml # Template configuration file
โโโ .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 |
Typos in source.type. Supported types: postgres, mysql, snowflake, sqlserver, oracle, databricks. |
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.2.tar.gz.
File metadata
- Download URL: db_schema_guard-0.1.2.tar.gz
- Upload date:
- Size: 41.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
104f9d905653678b4e6e786205c56abcccfec84dc03391f4b5fe13891915c002
|
|
| MD5 |
015a9ed7b162155ae2e2de6601830772
|
|
| BLAKE2b-256 |
384fa5e243bf502c4be0e75e77d8c21d67a4487880a39243e7894744cfccebd6
|
File details
Details for the file db_schema_guard-0.1.2-py3-none-any.whl.
File metadata
- Download URL: db_schema_guard-0.1.2-py3-none-any.whl
- Upload date:
- Size: 34.1 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 |
bcf3df75da33d989335fa3a31a1f7b67eb5f066cd3ea60d146fcbdef86f07be7
|
|
| MD5 |
8d4f3d98ef4e68dbf376e3e131c009a1
|
|
| BLAKE2b-256 |
8be86ee1ed93e0a50930685fb4a18362f99477e6d33b30f9ca29aad0e05f38c0
|