>
dbwarden
/ how it works

How Declarative Database
Migrations Work.

SQLAlchemy models → schema state → deterministic diff → SQL migration → rollback → verification. Configure the database once, then every change follows the same loop.

the operating sequenceEvery step leaves something
you can inspect.

No step happens invisibly: each one produces a file or a state you can open and check.

01
configure

Point dbwarden at your databases and models.

Run dbwarden init once: it creates the migrations/ layout and a declarative dbwarden.py, and it is safe to run again later, since it never touches the database. Declare each database with four required parameters, database_name, default, database_type, and database_url_sync, then point model_paths at the package that holds your models. dbwarden settings show prints what was resolved, so the config can be checked before anything else runs.

Read the guide ↗
example / 01
from dbwarden import DbwardenDatabase

class Primary(DbwardenDatabase):
    database_name = "primary"
    default = True
    database_type = "sqlite"
    database_url_sync = "sqlite:///./app.db"
    model_paths = ["app.models"]
$ dbwarden init
$ dbwarden settings show
02
declare

The models are the schema.

SQLAlchemy models are the authority. Mapped columns carry nullability, defaults, and keys; a typed class Meta keeps indexes, engines, codecs, and backend-specific options beside the table they describe. Meta is validated when the module loads, so an unknown attribute raises instead of producing wrong DDL later. This is the whole schema: dbwarden reads it, and nothing else describes the database.

Read the guide ↗
example / 02
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"
        indexes = [
            IndexSpec(name="ix_users_email", columns=["email"]),
        ]
03
derive

Generate the migration from the diff.

make-migrations compares the model state with the latest schema snapshot and falls back to the live database when no snapshot exists. It writes one versioned SQL file with -- upgrade and -- rollback sections, plus a companion .plan.json that records the typed operations and their severity. The output is deterministic: the same models and state always produce the same SQL, so reviews only see real changes.

Read the guide ↗
example / 03
$ dbwarden make-migrations "create core tables" --database primary
Created migration: migrations/primary/primary__0001_create_core_tables.sql
-- upgrade
ALTER TABLE users ADD COLUMN bio TEXT;

-- rollback
ALTER TABLE users DROP COLUMN bio;
04
inspect

Read the exact artifact before it ships.

Open the file and check both sections. dbwarden check reads the plan and classifies each change as INFO, WARNING, or ERROR, and dbwarden diff shows the structural difference read-only. Renames are emitted as ALTER TABLE ... RENAME COLUMN instead of a DROP plus ADD, so the review sees the exact statements that will run.

Read the guide ↗
example / 04
$ dbwarden check --database primary
INFO  add_column  users.bio

$ dbwarden diff --database primary
-- upgrade
ALTER TABLE users RENAME COLUMN name TO full_name;

-- rollback
ALTER TABLE users RENAME COLUMN full_name TO name;
05
apply

Apply the pending files, under lock.

migrate resolves the config, acquires the engine-native migration lock, records holder state and heartbeat progress, executes the pending SQL in order, records the migration with its checksum, and releases the lock. A schema snapshot is written for the next diff. For risky changes, --sandbox rehearses against a temporary database first, and --with-backup captures a pre-migration state before anything is applied.

Read the guide ↗
example / 05
$ dbwarden migrate --database primary
Applying migration: primary__0001_create_core_tables.sql
Migration applied successfully

$ dbwarden migrate --database primary --with-backup --backup-dir ./backups
06
verify

Confirm the database matches the models.

dbwarden status shows applied and pending counts, dbwarden history shows execution order and timestamps, and check-db reads the live schema directly. Together they answer whether the migration queue is clean and what the database actually holds. After a branch merge, status also exposes MERGE_PENDING; resolve it with merge before generating another migration. The loop closes when the models, the migration files, and the database agree.

Read the guide ↗
example / 06
$ dbwarden status --database primary
Database: primary
Applied migrations: 1
Pending migrations: 0

$ dbwarden history --database primary
1  primary__0001_create_core_tables.sql  applied
07
rollback

Reverse with the rollback that shipped with the file.

dbwarden rollback runs applied migrations in reverse order, executing the -- rollback section of each file under the same lock discipline as migrate, with --count or --to-version for partial rollbacks. Recovery quality is decided when the migration is written, not during the incident: the rollback ships in the same file as the upgrade.

Read the guide ↗
example / 07
$ dbwarden rollback --database primary --count 1
Rolling back migration: primary__0001_create_core_tables.sql
-- rollback
DROP INDEX IF EXISTS ix_posts_created_at;
DROP TABLE posts;
DROP TABLE users;
convergencemodel = database

The loop is complete when the models, the migration files, and the live database all agree.