If the app is Django, FastAPI, or Flask, and the migrations are still Flyway, you already know the shape of the job: versioned SQL files, a history table, CI that applies them in order. The awkward part is the JVM sitting next to a Python deploy.
DBLift is that same idea, in pip. Not a rewrite of your SQL. Not a new religion about ORMs.
What carries over, and what does not
Most of a Flyway project moves as-is. The exceptions are the parts that only exist because Flyway runs on the JVM.
| Flyway | DBLift |
|---|---|
V1__create_users.sql, R__views.sql, U1__drop_users.sql | Same files, same names, untouched |
| CRC32 checksums | Same algorithm, so unchanged SQL still matches |
flyway_schema_history | Imported into dblift_schema_history |
| Java migrations on the classpath | History is imported, the code does not run |
flyway.conf and JVM flags | One environment variable, or one YAML file |
The double underscore in file names is still mandatory. Everything else in this post is about the two things that actually change: how you point DBLift at a database, and what to do with Java.
Point it at a database
The shortest path is one environment variable. Every command in this post works once it is set.
pip install "dblift[postgresql]"
export DBLIFT_DB_URL="postgresql+psycopg://user:password@localhost:5432/mydb"
Swap the extra for mysql, sqlserver, or oracle if that is your engine.
Prefer a file? Put it in the project root. The CLI picks it up from the working directory when you pass neither a config path nor a URL.
database:
type: postgresql
url: "postgresql+psycopg://localhost:5432/app"
schema: public
migrations:
directories:
- "./migrations"
Three things to know about that file:
- The database block is required. The schema line is required for PostgreSQL, MySQL, SQL Server, and Oracle. SQLite and DuckDB have no schema to name.
- Leave the migrations block out and DBLift reads a folder named
migrationsnext to the file. - Credentials go in the environment (
DBLIFT_DB_PASSWORD) or a secret URI, not in the file.
Out-of-order versions. The default,
strict_mode: false, behaves like Flyway withoutOfOrder=true: a V2 that lands after V3 is pending and runs. Setstrict_mode: trueand the same V2 fails instead, which is closer to Flyway's default. The flag form is--strict, accepted by every command exceptimport-flyway.
Full reference: Configuration.
Bring the history with you
If the database already has a Flyway history table, import it. Do not baseline: baseline would collapse every applied version into a single row, and you would lose the per-version record Flyway built up.
dblift import-flyway # copy the Flyway history table
dblift info # applied vs pending, should match Flyway
dblift validate # warns about scripts the folder no longer has
Both table names are defaults. Custom names take the --flyway-table and --table flags.
Every row in the Flyway table is translated on the way in:
| Flyway row type | Imported as | Runs again? |
|---|---|---|
| SQL | versioned SQL | no |
| JDBC, SPRING_JDBC | versioned SQL | no |
| SCRIPT | versioned SQL | no |
| anything else | the import stops | — |
Java rows are history, not code. A Flyway Java migration is a compiled class on the classpath, typically
db.migration.V3__Foo. After import its version is marked applied, so migrate skips it. But there is no matching file in your migrations folder, and DBLift only executes.sqland.py. Validate warns about the missing file. Add--strictto turn the warning into an error, or rundblift repairto mark the script as deleted.Any Java or script migration that is still pending has to be rewritten as
V3__foo.sqlorV3__foo.pybefore your first migrate. Importing history does not compile anything. Keep.javasources out of the migrations folder.
Step-by-step version: Move from Flyway.
Python when SQL is not enough
A Python migration uses the same file name as a SQL one, with a different extension. It exposes one function.
from api import MigrationContext # dblift ships top-level packages
def migrate(context: MigrationContext) -> None:
if context.dry_run:
return
context.execute("UPDATE accounts SET active = true WHERE active IS NULL")
- Reach for Python when the change is data work or SDK calls that are painful in SQL. Relational engines accept both formats side by side.
- Undo is a separate
U1_0_0__….pywith its ownmigratefunction, not a second function in the same file. - Placeholders are not substituted in Python. Read them from
context.placeholders. - Cosmos DB and MongoDB accept Python only. A
.sqlfile there fails withDBLIFT-NOSQL-001before anything runs.
Full contract: Python migrations.
Django, Flask, FastAPI
Django
pip install "dblift[django]"
INSTALLED_APPS = [
# ...
"integrations.django",
]
DBLIFT_MIGRATIONS_DIR = BASE_DIR / "migrations"
# Optional. Without these, DBLift uses DATABASES["default"].
# DBLIFT_DATABASE_ALIAS = "default"
# DBLIFT_DATABASE_URL = "postgresql+psycopg://user:pass@host/db"
python manage.py dblift_migrate
python manage.py dblift_validate
python manage.py dblift_info
This sits next to Django's ORM migrations rather than on top of them. DBLift keeps its own history table and never touches django_migrations. The system check framework can warn when SQL is pending (check id dblift.W001), but a warning never applies anything. Apply stays a deploy step.
Flask and FastAPI
Flask gets a CLI command. Install the flask extra, call init_dblift(app, engine, "migrations") with your SQLAlchemy engine, register the CLI, and flask dblift-migrate is available.
FastAPI helpers are read-only. migration_guard tells you whether the database is current; it will not migrate on startup.
Docs: Django · Flask · FastAPI
Tests and CI
The pytest plugin is its own package, not an extra. Its dblift_migrated_db fixture applies your migrations before the tests run.
pip install pytest-dblift
On GitHub, the Action installs the pip package and runs one command. It does not start a database for you.
- uses: dblift/action@v1
with:
command: migrate # or validate, or info
extras: postgresql
Docs: CI/CD
If you already know Flyway
Same SQL files, same checksum idea, history you import instead of throwing away. Config is one environment variable or one YAML file. Java classes come across as history, not as runnable code; SQL and Python do run. Django, Flask, pytest, and GitHub Actions are extras and a plugin, not a second toolchain.