Migration File Format¶
Migration files are the execution contract in DBWarden.
Everything that changes your database should be represented in explicit SQL files that can be reviewed, tested, and rolled back.
File naming and location¶
Versioned migrations are stored under each database migrations directory (default: migrations/<database_name>).
Canonical filename pattern:
A legacy format without the {database_name}__ prefix (e.g. 0001_create_users_table.sql) is also accepted for backward compatibility.
Examples:
primary__0001_initial_schema.sql
primary__0002_add_users_table.sql
analytics__0001_create_events.sql
When a migration is auto-generated with make-migrations, DBWarden also writes a companion plan file:
That file captures machine-readable metadata for CI and debugging and is not executed by migrate.
Required sections¶
Each migration file must define both:
-- upgrade: statements applied duringmigrate-- rollback: statements applied duringrollback
If rollback is weak or incomplete, production recovery is weak or incomplete.
Migration classes¶
DBWarden supports three execution classes:
| Prefix | Class | Behavior |
|---|---|---|
NNNN_ |
Versioned | Runs once in ordered version sequence |
RA__ |
Runs always | Runs on every migrate execution |
ROC__ |
Runs on change | Runs when checksum changed |
When to use each¶
NNNN_: schema evolution (tables, columns, indexes, constraints)RA__: objects that should always be refreshed (views, grants)ROC__: routines/policies that should apply only when content changes
Execution model¶
At runtime, DBWarden builds an execution plan from file discovery + migration metadata:
- read versioned files and filter already-applied versions
- include
RA__files - include changed
ROC__files - execute with lock protection
- record metadata and checksums
Conceptual plan:
def build_plan(directory, applied_versions):
versioned = parse_versioned_files(directory)
repeatable = parse_repeatable_files(directory)
pending_versioned = [m for m in versioned if m.version not in applied_versions]
pending_ra = repeatable.runs_always
pending_roc = changed_only(repeatable.runs_on_change)
return pending_versioned + pending_ra + pending_roc
Examples¶
Versioned migration¶
-- upgrade
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
created_at DATETIME
);
-- rollback
DROP TABLE users;
Runs-always migration (RA__)¶
Filename example: primary__RA__refresh_active_users_view.sql
-- upgrade
CREATE OR REPLACE VIEW active_users AS
SELECT id, email FROM users WHERE is_active = TRUE;
-- rollback
DROP VIEW IF EXISTS active_users;
Runs-on-change migration (ROC__)¶
Filename example: primary__ROC__update_timestamp_trigger.sql
-- upgrade
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- rollback
DROP FUNCTION IF EXISTS update_updated_at();
Metadata headers¶
Headers are parsed from migration file comments. The -- seed marker is recognised
by tools; -- depends_on parsing is implemented but not yet enforced during
migration execution (migrations run in filesystem-sort order by version).
Dependency header (parsed but not enforced):
Seed marker:
Authoring guidelines¶
- One logical change per migration file
- Keep DDL explicit; avoid hidden application-side schema effects
- Keep rollback idempotent when possible (
IF EXISTS, safe predicates) - For data migrations, use bounded, reversible operations
- Prefer small migrations over large monolithic SQL scripts
Review checklist¶
Before merge:
- upgrade section matches intended schema change
- rollback section restores prior valid state
- indexes/constraints/defaults are explicit
- no environment-specific literals accidentally committed
Before release: