Database migration system for Python/SQLAlchemy projects
Project description
Migrations for FastAPI, without the DSL.
→ Full documentation | Source Code
DBWarden is a drop-in alternative to Alembic built for FastAPI: SQL-first migration files, async sessions wired directly into routes, and PostgreSQL round-trip fidelity out of the box.
Key Features
- SQL-first: Migrations are plain SQL. No DSL, no generated abstraction layer.
- Rollback included: Every migration carries both upgrade and rollback SQL.
- Schema snapshots: After every migration, a checksummed JSON snapshot is written. These snapshots power rename detection, offline migration generation, and column-level diffing without querying the live database.
- Column-level diffing: Type, nullable, default, and comment changes generate precise
ALTER COLUMNstatements. - Rich index metadata: Partial indexes (
WHERE), covering indexes (INCLUDE),USINGmethods,NULLS NOT DISTINCT, column sort order, storage parameters, and ClickHouse skip indexes. - FastAPI-native sessions:
session=primary.async_sessionas a route annotation — noDepends, noAnnotated, noSessionDep. - Single config source:
database_config(...)drives migrations, sessions, health checks, and seeds. - Dev mode: Run SQLite locally against a PostgreSQL production schema with automatic SQL translation.
- Multi-database: One project, multiple databases, full isolation.
- Sandbox & dry-run: Test migrations in a temporary database or preview SQL without touching anything.
- Observability: Prometheus metrics (6 families), JSON logging, FastAPI routers for
/metrics,/status,/migrate. - Versioned seeds: SQL and Python seed files with checksummed idempotent application.
- PostgreSQL first-class: Reverse-engineer a live database with
generate-models, feed intomake-migrations— zero diff. - ClickHouse first-class: Table options, replicated engines, dictionaries, materialized views, projections, skip indexes.
Requirements
- Python 3.10+
- SQLAlchemy
Installation
pip install dbwarden
pip install "dbwarden[fastapi]" # FastAPI integration
pip install "dbwarden[metrics]" # Prometheus metrics
pip install "dbwarden[sandbox]" # Docker-backed test databases
Quickstart
1. Configure
from dbwarden import database_config
primary = database_config(
database_name="primary",
default=True,
database_type="postgresql",
database_url_sync="postgresql://user:pass@localhost:5432/myapp",
database_url_async="postgresql+asyncpg://user:pass@localhost:5432/myapp",
)
2. Define your models
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime
from sqlalchemy.orm import declarative_base
from dbwarden import TableMeta
from dbwarden.schema import index
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String(255), unique=True, nullable=False)
bio = Column(Text, nullable=True)
class Meta(TableMeta):
comment = "Core user accounts"
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String(255), nullable=False)
body = Column(Text, nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, nullable=False)
class Meta(TableMeta):
indexes = [
index("ix_posts_created_at", ["created_at"]),
]
3. Generate a migration
dbwarden make-migrations
Output:
-- upgrade
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
bio TEXT
);
COMMENT ON TABLE users IS 'Core user accounts';
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS ix_posts_created_at ON posts (created_at);
-- rollback
DROP TABLE posts;
DROP TABLE users;
4. Apply
dbwarden migrate
5. Check status
dbwarden status
FastAPI Integration
Lifespan
from contextlib import asynccontextmanager
from fastapi import FastAPI
from dbwarden.fastapi import dbwarden_lifespan
@asynccontextmanager
async def lifespan(app: FastAPI):
async with dbwarden_lifespan(mode="check"):
yield
app = FastAPI(lifespan=lifespan)
On startup, dbwarden_lifespan validates the schema (or auto-applies migrations). On shutdown, it disposes all engine pools.
Per-Request Sessions
@app.get("/users")
async def list_users(session=primary.async_session):
result = await session.execute(select(User))
return result.scalars().all()
The handle from your config becomes a FastAPI dependency annotation. No Depends(), no Annotated, no SessionDep type alias.
Routers
from dbwarden.fastapi import DBWardenHealthRouter, DBWardenRouter
app.include_router(DBWardenHealthRouter(), prefix="/health")
app.include_router(DBWardenRouter(), prefix="/db")
Health router exposes readiness and liveness endpoints. DBWardenRouter exposes GET /status and POST /migrate.
Auto-Generated Schemas
Decorate a model with @auto_schema to get Pydantic CreateSchema and PublicSchema for request validation and API responses:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
from dbwarden import TableMeta
from dbwarden.schema import auto_schema
Base = declarative_base()
@auto_schema
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String(255), nullable=False)
password_hash = Column(String(255), nullable=False)
class Meta(TableMeta):
class password_hash:
public = False
# In a route:
data = User.CreateSchema(email="a@b.com", password_hash="secret")
user = User.from_schema(data)
session.add(user)
await session.commit()
api_result = user.to_schema() # PublicSchema — excludes password_hash
CreateSchema derives required fields from nullable columns. PublicSchema omits fields with public = False. No separate schema definitions to maintain.
PostgreSQL First-Class
Reverse-engineer a live PostgreSQL database with generate-models, feed the output back into make-migrations, and get zero diff:
dbwarden generate-models -d primary --tables users
dbwarden make-migrations
# → No changes detected
The round-trip is confirmed — your generated models match the database schema exactly. The following PostgreSQL features are fully supported:
- Identity columns with sequence options
- Generated columns (
GENERATED ALWAYS AS (...) STORED) - Per-column collation, storage, and compression
- Table fillfactor, tablespace, unlogged tables, and partitioning
- Table inheritance and exclusion constraints
- Deferrable foreign keys and check constraints with
NO INHERIT - Deferred unique constraints with
NULLS NOT DISTINCTandINCLUDE - Index options:
USING,WHERE,INCLUDE,WITH,NULLS NOT DISTINCT, column sort order - Named enum types with
ALTER TYPE ... ADD VALUE - Type normalization:
SERIAL,TIMESTAMPTZ,NUMERIC(p,s),VARCHAR(n),JSONB,UUID,ARRAY,TSTZRANGE
Observability
from dbwarden.fastapi import MetricsRouter, MetricsMiddleware
app.include_router(MetricsRouter())
app.add_middleware(MetricsMiddleware)
| Metric | Type | What it tracks |
|---|---|---|
dbwarden_migrations_total |
Counter | Migrations applied, by database and version |
dbwarden_migration_duration_seconds |
Histogram | Duration per migration |
dbwarden_schema_version |
Gauge | Current schema version per database |
dbwarden_seed_version |
Gauge | Current seed version per database |
dbwarden_migrations_pending |
Gauge | Pending migration count |
dbwarden_migration_errors_total |
Counter | Migration errors by type |
Sandbox & Dry-Run
dbwarden migrate --dry-run # Preview SQL without execution
dbwarden migrate --sandbox # Apply in a temporary in-memory SQLite database
dbwarden migrate --sandbox -d pg # Apply in a Docker-backed PostgreSQL sandbox
Versioned Seeds
dbwarden seed create "Initial Countries"
dbwarden seed apply
dbwarden seed list
dbwarden seed rollback
Seeds are tracked in a _dbwarden_seeds table with checksums, making them idempotent.
Generate Models
# One file per table:
dbwarden generate-models -d primary --tables users,orders
# Single file with ClickHouse engine metadata:
dbwarden generate-models -d clickhouse-db --clickhouse-engines --single-file
Multi-Database
primary = database_config(
database_name="primary",
default=True,
database_type="postgresql",
database_url_sync="postgresql://user:pass@localhost:5432/main",
database_url_async="postgresql+asyncpg://user:pass@localhost:5432/main",
model_paths=["models/primary"],
)
analytics = database_config(
database_name="analytics",
database_type="postgresql",
database_url_sync="postgresql://user:pass@localhost:5432/analytics",
database_url_async="postgresql+asyncpg://user:pass@localhost:5432/analytics",
model_paths=["models/analytics"],
)
dbwarden migrate --all
Dev Mode
primary = database_config(
...
dev_database_type="sqlite",
dev_database_url="sqlite:///./dev.db",
)
dbwarden --dev migrate
Supported Databases
| Database | database_type value |
|---|---|
| PostgreSQL | postgresql |
| MySQL | mysql |
| MariaDB | mariadb |
| SQLite | sqlite |
| ClickHouse | clickhouse |
License
MIT
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 dbwarden-0.8.3.tar.gz.
File metadata
- Download URL: dbwarden-0.8.3.tar.gz
- Upload date:
- Size: 682.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1e8a4f506084cdcfe8a71f18ad831756c759edb18c3bf331b21c29b9548d707
|
|
| MD5 |
fdc0f96d40379a0ea744624d40641e08
|
|
| BLAKE2b-256 |
47a05290a99d08fbc78a2452cc258f904adcea64938cb440ed223ad4dc273744
|
File details
Details for the file dbwarden-0.8.3-py3-none-any.whl.
File metadata
- Download URL: dbwarden-0.8.3-py3-none-any.whl
- Upload date:
- Size: 142.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be170f4237e20ea2d22369c64f783033ca0ce8a14a43f50740fc163465290930
|
|
| MD5 |
f461777000ed2a83a3e39901ec6a93e4
|
|
| BLAKE2b-256 |
f8308544a3bbe60842f697e5e721a1b4ad6db60afa98d4e3d08f58c6906d3027
|