Flyway for a Python shop

Same versioned SQL as Flyway, in pip. Import the history, keep the files, rewrite pending Java as SQL or Python, run migrate next to Django tests.

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.

FlywayDBLift
V1__create_users.sql, R__views.sql, U1__drop_users.sqlSame files, same names, untouched
CRC32 checksumsSame algorithm, so unchanged SQL still matches
flyway_schema_historyImported into dblift_schema_history
Java migrations on the classpathHistory is imported, the code does not run
flyway.conf and JVM flagsOne 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 migrations next 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 with outOfOrder=true: a V2 that lands after V3 is pending and runs. Set strict_mode: true and the same V2 fails instead, which is closer to Flyway's default. The flag form is --strict, accepted by every command except import-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 typeImported asRuns again?
SQLversioned SQLno
JDBC, SPRING_JDBCversioned SQLno
SCRIPTversioned SQLno
anything elsethe 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 .sql and .py. Validate warns about the missing file. Add --strict to turn the warning into an error, or run dblift repair to mark the script as deleted.

Any Java or script migration that is still pending has to be rewritten as V3__foo.sql or V3__foo.py before your first migrate. Importing history does not compile anything. Keep .java sources 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__….py with its own migrate function, 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 .sql file there fails with DBLIFT-NOSQL-001 before 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.

DBLift is information technology / developer tools software. Contact: contact@dblift.com.