In most migration workflows, the schema is described twice: once in the models, once in the migration scripts, and the two drift unless someone keeps reconciling them. The disagreement usually turns up in production, at the moment the database is asked to change. dbwarden keeps one definition: the SQLAlchemy models your application already imports.
Everything the database needs to look like is expressed there, including backend-specific options. The class Meta inner class holds comments, indexes, engines, and codecs beside the table they describe, and column-level Meta classes carry comments and visibility flags. All of it is validated when the module loads: an unknown attribute raises DBWardenConfigError instead of producing wrong DDL later.
make-migrations diffs that model state against the latest schema snapshot, falling back to the live database when no snapshot exists yet, and writes one versioned SQL file with an -- upgrade section and a -- rollback section.
The diff is canonicalized before comparison. A model says String(255) while PostgreSQL reports character varying(255); a Boolean default can render as false or FALSE; an identifier may be quoted in SQL and bare in model metadata; ClickHouse engine parameters can come back in a different order than they were declared. Handlers normalize both sides to the same representation first, so equivalent states produce no fake diffs, and a review never churns on whitespace or spelling that the database treats as identical.
Because generation starts from committed state, it is deterministic: the same model state and snapshot produce the same SQL every run. The migration file is derived output. Old migrations stay useful for review and deployment, but they no longer define the schema.
what one command derives-- upgrade
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
bio TEXT
);
CREATE TABLE IF NOT EXISTS posts (
id SERIAL 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 INDEX IF EXISTS ix_posts_created_at;
DROP TABLE posts;
DROP TABLE users;from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from dbwarden.databases import IndexSpec, TableMeta
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
bio: Mapped[str | None] = mapped_column(Text, nullable=True)
class Meta(TableMeta):
comment = "Core user accounts"
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class Meta(TableMeta):
indexes = [
IndexSpec(name="ix_posts_created_at", columns=["created_at"]),
]from dbwarden.databases.pgsql import PGTableMeta, PGColumnMeta, pg
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
bio: Mapped[str] = mapped_column(Text)
class Meta(PGTableMeta):
pg_fillfactor = 80
class id(PGColumnMeta):
pg = pg.field(identity="always", identity_start=100)