Prev Next

Database / Alembic DB migration tool Interview questions

Last updated

1. What is Alembic? 2. Why do we need a database migration tool like Alembic? 3. How do you install Alembic? 4. What does the alembic init command do? 5. What is the purpose of alembic.ini? 6. What is the purpose of env.py in Alembic? 7. What is a revision in Alembic? 8. What are revision and down_revision in a migration file? 9. How do you create a new migration script? 10. What are the upgrade() and downgrade() functions? 11. How do you apply migrations with alembic upgrade? 12. How do you roll back a migration in Alembic? 13. What is the alembic_version table? 14. What is the purpose of script.py.mako? 15. What does the alembic current command show? 16. What does the alembic history command show? 17. What is a head revision in Alembic? 18. How do you set the database URL in Alembic? 19. What is the op object in Alembic? 20. How do you create a table in an Alembic migration? 21. How does alembic revision --autogenerate work? 22. Which changes can autogenerate not detect? 23. How do you configure target_metadata in env.py? 24. What is the difference between offline and online mode in Alembic? 25. How do you generate a SQL script without connecting to the database? 26. How do you add a NOT NULL column to a table that already has rows? 27. What is batch_alter_table and why is it used with SQLite? 28. How do you run raw SQL in an Alembic migration? 29. How do you perform data migrations safely in Alembic? 30. What is the alembic stamp command used for? 31. How do relative and partial revision identifiers work in Alembic? 32. What happens when Alembic finds multiple head revisions? 33. How do you merge multiple heads in Alembic? 34. What is the difference between Alembic and Flask-Migrate? 35. How do you use Alembic with a FastAPI project? 36. What is the difference between Alembic and Django migrations? 37. Why should you use a naming convention with Alembic? 38. How do you rename a column in Alembic without losing data? 39. How do compare_type and compare_server_default affect autogenerate? 40. How do you exclude tables from autogenerate using include_object? 41. How do you manage migrations for multiple databases with Alembic? 42. What is the difference between Alembic and Flyway or Liquibase? 43. Explain the execution flow of alembic upgrade head? 44. Explain the internal working of Alembic autogenerate? 45. How do branch labels and depends_on work in Alembic? 46. How do you run Alembic with an async SQLAlchemy engine? 47. How do you troubleshoot "Can't locate revision identified by" error? 48. How can you avoid locking large tables with Alembic on PostgreSQL? 49. How do you run Alembic migrations safely in CI/CD? 50. What is transactional DDL and how does Alembic handle it?

1. What is Alembic?

Alembic is a lightweight database migration tool for Python, created by Mike Bayer, the author of SQLAlchemy. It records schema changes as versioned Python scripts so every environment can be brought to the same structure in a repeatable way.

Each script is called a revision and contains an upgrade() and a downgrade() function. Alembic stores the current revision in a table named alembic_version, which is how it knows which scripts are still pending.

Because it sits on top of SQLAlchemy, it works with PostgreSQL, MySQL, SQLite, Oracle, SQL Server and any other backend SQLAlchemy supports. It can also draft migrations for you by comparing your models with the live database (autogenerate).

Take quiz
Which library is Alembic built on?
Django ORM
Hibernate
SQLAlchemy
Peewee
Where does Alembic remember which revision a database is on?
In alembic.ini
In a hidden .alembic_state file
In the Mako template
In the alembic_version table

2. Why do we need a database migration tool like Alembic?

Hand-run SQL scripts drift apart. One developer adds a column locally, someone forgets to run it on staging, and production breaks on release day. A migration tool turns schema changes into version-controlled, ordered steps that every environment applies the same way.

Alembic gives you:

  • Repeatability - one command builds the current schema from an empty database.
  • Reviewability - migrations are plain files that go through pull requests.
  • Reversibility - each revision can define how to undo itself.
  • Traceability - the database knows exactly which revision it is on.

It also removes the temptation to call create_all() in production, which never alters existing tables.

Take quiz
What is the main risk of applying schema changes by hand in each environment?
Slower SQL execution
Larger Python packages
Loss of Python type hints
Schema drift between dev, staging and production
Why is create_all() a poor replacement for migrations in production?
It does not alter tables that already exist
It deletes the alembic_version table
It only works with SQLite
It needs a Mako template

3. How do you install Alembic?

Install it from PyPI inside your project's virtual environment:

pip install alembic
alembic --version

Pip pulls in SQLAlchemy, Mako and typing_extensions automatically. You still need a database driver for your backend, for example psycopg2-binary for PostgreSQL or pymysql for MySQL.

Pin the version in requirements.txt or pyproject.toml so every developer and CI job uses the same release. After installing, the alembic command is available on your PATH.

If you use Poetry or uv, the equivalent is poetry add alembic or uv add alembic. Installing inside the same environment as your app matters, because env.py imports your models.

Take quiz
Which command installs Alembic?
pip install alembic
pip install python-alembic-cli
pip install sqlalchemy-migrate-alembic
npm install alembic
What else is needed to run migrations against PostgreSQL?
Only Mako
A driver such as psycopg2
A separate migration server
The Django admin package

4. What does the alembic init command do?

alembic init <directory> creates a migration environment in your project. It writes files and folders only; it never touches the database.

Generated item Role
alembic.ini CLI configuration (script location, URL, logging)
alembic/env.py Python script that connects and runs migrations
alembic/script.py.mako Template for new revision files
alembic/versions/ Folder that holds revision scripts
alembic/README Short note about the chosen template

Use -t async for an async SQLAlchemy engine or -t multidb for several databases:

alembic init alembic
alembic init -t async migrations

Take quiz
Which folder holds revision scripts after alembic init?
revisions_out
versions
db_scripts
history
Does alembic init create tables in the database?
Yes, it creates all model tables
Yes, it creates alembic_version immediately
No, it only creates files and folders
Only when -t async is used

5. What is the purpose of alembic.ini?

alembic.ini is the configuration file the alembic command reads at startup. It tells Alembic where the migration folder is and how to name and log things.

  • script_location - path to the migration folder
  • sqlalchemy.url - default database URL
  • file_template - naming pattern for revision files
  • prepend_sys_path - adds your project to the import path
  • version_locations - extra folders for revisions
  • [loggers] sections - logging setup used by env.py

It is parsed with Python's configparser, so a literal % must be written as %%, which matters for URL-encoded passwords. Use -c other.ini to point at a different file.

Take quiz
Which alembic.ini setting points to the migration folder?
version_table_dir
migration_root
script_location
env_path
How do you write a literal % inside alembic.ini?
As \%
As %25 only
It is not allowed
As %%

6. What is the purpose of env.py in Alembic?

env.py is the Python script Alembic executes every time you run a command such as upgrade or revision --autogenerate. It decides how Alembic connects to the database and what metadata to compare against.

A standard file has two functions, run_migrations_offline() and run_migrations_online(), and picks one with context.is_offline_mode(). It also loads logging config and reads alembic.ini values through context.config.

from alembic import context
config = context.config
target_metadata = Base.metadata

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

This is the right place to inject the database URL from an environment variable or to set options like compare_type.

Take quiz
When does Alembic run env.py?
Only once, during alembic init
Only when the app server starts
Only for downgrades
Each time a command like upgrade or revision --autogenerate is executed
Which object in env.py exposes alembic.ini values?
context.config
op.get_bind()
metadata.bind
script.ini_file

7. What is a revision in Alembic?

A revision is one migration script in the versions/ folder. It represents a single, named step in the schema history and carries an ID, a link to its parent, and the upgrade() / downgrade() code.

IDs are short random hex strings such as ae1027a6acf, so two developers can create migrations on separate branches without number clashes. Revisions form a chain through their parent links, not through file names.

"""create users table

Revision ID: 3f2a9c1d7e45
Revises: 
Create Date: 2026-11-11 09:30:00
"""
revision = "3f2a9c1d7e45"
down_revision = None

The docstring, including the message you passed with -m, is what alembic history prints.

Take quiz
What does a single Alembic revision file represent?
One versioned step of schema changes with upgrade and downgrade code
A full database backup
A SQLAlchemy model class
A set of environment variables
How are revision IDs normally produced?
As sequential integers 1, 2, 3
As short random hex strings
From the file's modification time
From the Git commit hash

8. What are revision and down_revision in a migration file?

revision is the unique ID of the current script. down_revision is the ID of its parent, the migration that must run before it. The very first revision sets down_revision = None.

Alembic builds the migration order by following these links, so file names and timestamps do not matter. A merge revision uses a tuple for down_revision because it has two parents.

# 2nd migration
revision = "b7c2d4e8f901"
down_revision = "3f2a9c1d7e45"

# merge migration
revision = "c9d1e3f50a12"
down_revision = ("b7c2d4e8f901", "a1b2c3d4e5f6")

Editing down_revision by hand is how you re-order or re-attach a migration, but it must be done before the revision is applied anywhere shared.

Take quiz
What value does the first migration use for down_revision?
'base'
None
'0000'
An empty tuple
What does a merge revision use for down_revision?
A comma-separated filename
The string 'heads'
A tuple of the parent revision IDs
Nothing, it omits the field

9. How do you create a new migration script?

Run alembic revision -m "message". Alembic creates a new file in versions/ using script.py.mako, links it to the current head, and leaves empty upgrade() and downgrade() bodies.

  1. Run alembic revision -m "add orders table".
  2. Open the generated file.
  3. Write the operations with op.* calls.
  4. Test with alembic upgrade head and alembic downgrade -1.

Add --autogenerate to let Alembic draft the body from your models. Other useful flags are --rev-id for a custom ID, --head to choose the parent, and --branch-label for branched histories.

alembic revision -m "add orders table"
alembic revision --autogenerate -m "add orders table"

Take quiz
Which command creates an empty migration with a message?
alembic new -m "add orders"
alembic create "add orders"
alembic revision -m "add orders"
alembic migrate --empty
Which flag asks Alembic to compare models with the database?
--diff
--detect
--sync
--autogenerate

10. What are the upgrade() and downgrade() functions?

upgrade() applies the schema change when Alembic moves forward past this revision. downgrade() reverses it when Alembic moves backward. Both are plain Python functions that call op methods.

def upgrade():
    op.add_column("users", sa.Column("phone", sa.String(20)))

def downgrade():
    op.drop_column("users", "phone")

A good downgrade() undoes the steps in reverse order. Some changes cannot be undone safely, for example a dropped column that held data. In that case raise an exception or document it, rather than leaving a silent pass.

Alembic never calls both in one run. Which one executes depends only on the direction of the command, upgrade or downgrade.

Take quiz
Which function runs when Alembic moves the database to an older revision?
upgrade()
revert()
rollback()
downgrade()
What should downgrade() normally contain?
The inverse of upgrade(), in reverse order
A copy of upgrade()
A DROP DATABASE statement
Only the revision ID

11. How do you apply migrations with alembic upgrade?

alembic upgrade head reads the current revision from alembic_version, finds every pending revision up to the target, runs each upgrade() in order, and updates the version table.

Command Effect
alembic upgrade head Apply everything up to the newest revision
alembic upgrade +1 Apply only the next pending revision
alembic upgrade ae10 Go to a revision (a unique prefix is enough)
alembic upgrade ae10+2 Two steps after that revision

If the database is already at the target, nothing happens, which makes the command safe to run on every deploy.

Take quiz
What does alembic upgrade +1 do?
Applies only the next pending revision
Applies all pending revisions
Creates a new revision
Upgrades the Alembic package
What does 'head' refer to in alembic upgrade head?
The first revision
The newest revision in the chain
The revision currently in the database
The largest migration file

12. How do you roll back a migration in Alembic?

Use alembic downgrade. Alembic runs the downgrade() of each revision from the current one back to the target, then updates alembic_version.

alembic downgrade -1      # undo the last revision
alembic downgrade ae10   # go back to a specific revision
alembic downgrade base   # undo everything

Rollbacks can lose data, because a downgrade that drops a column or table cannot bring the values back. Test downgrades in a copy of production data, and in live systems many teams prefer to roll forward with a fix migration instead.

Take quiz
Which command undoes the most recently applied revision?
alembic rollback
alembic downgrade -1
alembic undo
alembic upgrade -1
What does alembic downgrade base do?
Deletes the versions folder
Resets alembic.ini
Reverts all revisions back to the empty starting point
Reverts only the latest revision

13. What is the alembic_version table?

alembic_version is the bookkeeping table Alembic creates on the first migration. It has one column, version_num (VARCHAR(32)), and normally one row holding the current revision ID.

After each successful migration step Alembic updates that row. If the history has branches, there can be several rows, one per head. Because the column is 32 characters wide, very long custom revision IDs will fail to insert.

You can rename the table with version_table and place it in another schema with version_table_schema inside context.configure().

context.configure(
    connection=connection,
    target_metadata=target_metadata,
    version_table="schema_history",
)

Take quiz
What does the alembic_version table store?
A checksum of every SQL statement
A log of every user login
The ID of the revision the database is currently at
A list of model classes
Which context.configure option renames the version table?
version_name
state_table
tracking_table
version_table

14. What is the purpose of script.py.mako?

script.py.mako is the Mako template Alembic renders whenever it creates a new revision file. It controls the header, imports and function skeletons that every migration starts with.

"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

def upgrade():
    ${upgrades if upgrades else "pass"}

def downgrade():
    ${downgrades if downgrades else "pass"}

Edit it when you want every migration to import something extra, such as import sqlmodel, or to add a team header. Changes affect only new revisions, not existing files.

Take quiz
Which template engine renders new revision files?
Jinja2
Django templates
Handlebars
Mako
Why would you edit script.py.mako?
To change the boilerplate that every new revision starts with
To choose the database URL
To decide migration order
To disable downgrade()

15. What does the alembic current command show?

alembic current prints the revision ID(s) the connected database is at, by reading the alembic_version table. A (head) marker means the database is already on the newest revision.

$ alembic current
b7c2d4e8f901 (head)

Add -v for the full message and parent. If nothing is printed, the database has never been migrated or stamped. Since it queries the database, it needs a working connection, unlike alembic history.

It is the quickest sanity check after a deploy: compare its output with alembic heads. If the two IDs match, the database is fully migrated.

Take quiz
What does the (head) marker next to alembic current output mean?
The database is on the newest revision
The last migration failed
Two heads exist
Alembic is in offline mode
Does alembic current need a database connection?
No, it reads only version files
Yes, it reads alembic_version
No, it reads only alembic.ini
Only when --sql is passed

16. What does the alembic history command show?

alembic history lists every revision in the chain from newest to oldest, showing parent, ID and message. It reads the files in versions/ and does not need a database connection.

$ alembic history -i
b7c2d4e8f901 -> c9d1e3f50a12 (head), add orders
3f2a9c1d7e45 -> b7c2d4e8f901 (current), add phone
<base> -> 3f2a9c1d7e45, create users

  • -v shows full details
  • -r base:head limits the range
  • -i marks the current database revision, which does need a connection

Because it only reads files, it is safe to run in CI or on a laptop with no database, which makes it handy for spotting forked histories.

Take quiz
Where does alembic history read its data from?
The alembic_version table only
The revision scripts in the versions folder
information_schema
pip metadata
Which flag marks the database's current revision in history output?
--mark-db
-d
-i (--indicate-current)
--current-only

17. What is a head revision in Alembic?

A head is a revision that no other revision lists as its down_revision, in other words the newest tip of a chain. In a linear history there is exactly one head, and head is shorthand for it.

List them with alembic heads. If two developers create migrations from the same parent, you end up with two heads, and alembic upgrade head fails with a "Multiple head revisions" error.

Term Meaning
head The single newest revision
heads All head revisions, used with multiple branches
base The empty state before the first revision

Fix it with alembic merge, or upgrade to heads if the branches are truly independent.

Take quiz
Which revision counts as a head?
The one with the oldest date
The one named 'initial'
One that no other revision uses as its down_revision
The one currently in alembic_version only
What happens with alembic upgrade head when two heads exist?
It picks the newest file by date
It applies only the first alphabetically
It deletes the older branch
It fails and reports multiple head revisions

18. How do you set the database URL in Alembic?

The simplest way is the sqlalchemy.url key in alembic.ini. That works for local work, but committing real credentials is a bad idea, so most projects override it in env.py.

import os
config = context.config
config.set_main_option(
    "sqlalchemy.url", os.environ["DATABASE_URL"]
)

A third option is a custom command-line argument read with context.get_x_argument(as_dictionary=True):

alembic -x db=staging upgrade head

Remember that set_main_option goes through configparser, so escape any % in the URL as %%.

Whichever option you pick, keep one source of truth. Mixing an ini value and an environment variable is how migrations end up running against the wrong database.

Take quiz
Where can you override sqlalchemy.url to avoid committing credentials?
In script.py.mako
In the alembic_version table
In the versions folder name
In env.py, reading an environment variable
Which flag passes a custom argument that env.py can read?
-x
-e
--url-extra
-p

19. What is the op object in Alembic?

op (from alembic import op) is a proxy to the Operations object bound to the migration currently running. Every schema change inside upgrade() and downgrade() is a method call on it.

Method Purpose
op.create_table() Create a table
op.add_column() Add a column
op.alter_column() Change type, nullability, default or name
op.create_index() Create an index
op.execute() Run raw SQL
op.batch_alter_table() Batch mode for SQLite

The same call renders different SQL for different databases, because Alembic picks the dialect from the connection. op.get_bind() returns the active connection when you need to query data.

Take quiz
What does the op object represent in a migration script?
The Operations interface for the migration being run
The SQLAlchemy ORM session
The Mako template
The alembic.ini parser
Which op method runs a raw SQL statement?
op.raw()
op.execute()
op.sql_run()
op.query()

20. How do you create a table in an Alembic migration?

Call op.create_table() in upgrade() with the table name and sa.Column objects, and mirror it with op.drop_table() in downgrade().

def upgrade():
    op.create_table(
        "orders",
        sa.Column("id", sa.Integer, primary_key=True),
        sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False),
        sa.Column("total", sa.Numeric(10, 2), nullable=False),
        sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
    )
    op.create_index("ix_orders_user_id", "orders", ["user_id"])

def downgrade():
    op.drop_index("ix_orders_user_id", table_name="orders")
    op.drop_table("orders")

Give constraints and indexes explicit names. Unnamed ones get database-generated names that differ between backends and are painful to drop later.

If you generated the migration with --autogenerate, the same create_table call appears automatically, so this is mainly useful when you write or fix a migration by hand.

Take quiz
Which call creates a table inside upgrade()?
op.new_table()
op.create_table()
op.add_table()
sa.create_table()
Why should constraints and indexes be given explicit names?
Alembic refuses unnamed ones
Names make queries faster
Generated names vary by backend and are hard to drop later
It is required by SQLAlchemy Core

21. How does alembic revision --autogenerate work?

Autogenerate compares the MetaData in your models (target_metadata) with the schema reflected from the live database, then writes the differences as op calls into a new revision file.

  1. Alembic runs env.py and connects to the database.
  2. It reflects the existing tables, columns, indexes and foreign keys.
  3. It diffs them against target_metadata.
  4. It renders the diff as Python in upgrade() and the inverse in downgrade().

The output is a draft. Always read it, because renames show up as a drop plus an add, and some changes are not detected at all. The database must be at the latest revision first, otherwise Alembic refuses to autogenerate.

alembic upgrade head
alembic revision --autogenerate -m "add phone to users"

Take quiz
What does autogenerate compare to produce a migration?
Two Git commits
Two alembic.ini files
Model metadata versus the reflected live database schema
The previous and next revision files
How does autogenerate usually represent a column rename?
As an alter_column rename
As a no-op
As a table rebuild
As a drop of the old column and an add of the new one

22. Which changes can autogenerate not detect?

Autogenerate is good at structure but blind to anything it cannot reflect or infer. Treat the generated file as a starting point.

Detected Not detected
Added or removed tables and columns Table or column renames (seen as drop + add)
Nullable changes Anonymous or unnamed constraints
Basic index and unique constraint changes CHECK constraints
Foreign key changes Views, triggers, stored procedures
Column type changes (compare_type) Data changes and some Enum or custom type changes

Server defaults are only compared when compare_server_default=True is set. If you rely on any item in the right column, write those migrations by hand.

Take quiz
How does autogenerate see a renamed column?
As a rename operation
It ignores the change
It raises an error
As a removal plus an addition
Which of these is not detected by autogenerate?
CHECK constraints
Added tables
Dropped columns
Nullable changes

23. How do you configure target_metadata in env.py?

Set target_metadata to the MetaData object your models are registered on, then pass it to context.configure(). Autogenerate uses it as the 'desired' schema.

from myapp.db import Base
import myapp.models  # noqa: registers all model classes

target_metadata = Base.metadata

context.configure(
    connection=connection,
    target_metadata=target_metadata,
)

The most common mistake is forgetting to import the model modules. An unimported model is not on the metadata, so autogenerate thinks the table should be dropped. With several metadata objects, target_metadata can be a list.

If it is left as None, autogenerate produces nothing useful.

Take quiz
What is target_metadata normally set to?
The MetaData of your declarative Base
The sqlalchemy.url string
The alembic_version table
A Mako template object
What happens if a model module is never imported in env.py?
Alembic imports it automatically
Autogenerate may propose dropping its table
The upgrade crashes at init
The model is created twice

24. What is the difference between offline and online mode in Alembic?

Online mode connects to the database and executes the migration. Offline mode never connects; it emits the SQL as text, usually to a file or stdout, so a DBA can review and run it.

Aspect Online Offline
Connection Live engine None, URL only for dialect
Output Changes applied SQL script
Command alembic upgrade head alembic upgrade head --sql
Function in env.py run_migrations_online() run_migrations_offline()

Offline mode uses literal_binds=True so values are written into the SQL instead of bound parameters. Anything in a migration that queries data, such as op.get_bind().execute(select...), cannot work offline.

Take quiz
Which flag switches Alembic to offline mode?
--dry
--sql
--offline-only
--print
Why can data-querying code break in offline mode?
Offline mode skips Python
SQL is encrypted
There is no database connection to run the query on
Only DDL is parsed

25. How do you generate a SQL script without connecting to the database?

Add --sql to upgrade or downgrade. Alembic runs in offline mode and prints the SQL, including the alembic_version updates.

alembic upgrade head --sql > release.sql
alembic upgrade ae10:head --sql > patch.sql
alembic downgrade head:ae10 --sql > rollback.sql

Without a database Alembic cannot read the current revision, so a plain upgrade head --sql starts from base. For an incremental script, give an explicit start:end range.

This is the usual route when production changes must pass through a DBA or a change-approval process.

Take quiz
What is needed to produce an incremental SQL script in offline mode?
A second alembic.ini
The --dry-run flag
A start:end revision range
A running database
Where does a plain 'alembic upgrade head --sql' start from?
From the last Git tag
From the newest file
From the previous run's output
From base, because the current revision is unknown

26. How do you add a NOT NULL column to a table that already has rows?

A direct add_column(..., nullable=False) fails on a populated table because existing rows have no value. Use a three-step approach.

  1. Add the column as nullable (or with a server_default).
  2. Backfill existing rows with an UPDATE.
  3. Alter the column to nullable=False.
def upgrade():
    op.add_column("users", sa.Column("status", sa.String(10), nullable=True))
    op.execute("UPDATE users SET status = 'active'")
    op.alter_column("users", "status", existing_type=sa.String(10), nullable=False)

On big tables, run the backfill in batches to avoid long locks. Supplying server_default is shorter when a constant value is fine, and you can drop the default afterwards if the app should always provide the value.

Take quiz
Why does adding a NOT NULL column without a default fail on a populated table?
Alembic blocks NOT NULL
SQL does not allow NOT NULL on any existing table
The alembic_version table locks it
Existing rows would have no value for it
What is the correct order for the safe approach?
Add nullable, backfill, then set NOT NULL
Set NOT NULL, add column, backfill
Backfill, add column, drop table
Add NOT NULL, stamp, upgrade

27. What is batch_alter_table and why is it used with SQLite?

SQLite supports only a few ALTER TABLE forms. It cannot drop a column in older versions, change a type, or add a constraint to an existing table. op.batch_alter_table() works around this with move and copy.

  1. Create a new temporary table with the desired structure.
  2. Copy the data across.
  3. Drop the old table.
  4. Rename the new table to the original name.
with op.batch_alter_table("users") as batch_op:
    batch_op.alter_column("age", type_=sa.BigInteger)
    batch_op.drop_column("legacy_flag")

Set render_as_batch=True in context.configure() so autogenerate emits batch blocks automatically. Unnamed constraints need a naming_convention, otherwise batch mode cannot refer to them. On other databases, batch mode just runs ordinary ALTER statements.

Take quiz
How does batch_alter_table change a table on SQLite?
It creates a new table, copies data, drops the old one and renames
It sends ALTER TABLE ... FORCE
It edits the SQLite file header
It uses a stored procedure
Which context.configure option makes autogenerate emit batch blocks?
batch_mode=True
render_as_batch=True
sqlite_safe=True
use_batch=1

28. How do you run raw SQL in an Alembic migration?

Use op.execute() with a SQL string or a SQLAlchemy construct. It works in both online and offline modes.

from sqlalchemy import text

def upgrade():
    op.execute("UPDATE orders SET status = 'new' WHERE status IS NULL")
    op.execute(text("CREATE VIEW active_users AS SELECT * FROM users WHERE active"))

def downgrade():
    op.execute("DROP VIEW active_users")

Views, triggers, functions and CHECK constraints are typical reasons for raw SQL, since autogenerate cannot produce them. For bulk inserts of static data, op.bulk_insert() is cleaner. Avoid string-formatting user values into SQL; use bound parameters through text() and op.get_bind() when values vary.

Take quiz
Which function runs a raw SQL statement in a migration?
op.sql()
op.execute()
op.raw_query()
op.text_run()
Why is raw SQL often needed for views and triggers?
Alembic forbids op.create_view
They only exist in SQLite
Autogenerate cannot detect or create them
They are stored in env.py

29. How do you perform data migrations safely in Alembic?

Treat data changes as separate, small and repeatable. The biggest rule: do not import your application's ORM models into a migration. Models change over time and would break old revisions.

Define a lightweight table in the migration itself:

users = sa.table("users", sa.column("id", sa.Integer), sa.column("email", sa.String))

def upgrade():
    conn = op.get_bind()
    conn.execute(
        users.update().values(email=sa.func.lower(users.c.email))
    )

  • Keep schema and data steps in separate revisions when possible.
  • Batch updates on large tables, for example by primary key ranges.
  • Write a real downgrade() or document why it is impossible.
  • Use op.bulk_insert() for seed or lookup rows.
Take quiz
Why avoid importing ORM models inside a migration?
Models cannot be imported in Python files
Alembic rejects them at init
Models change later and can break old revisions
ORM classes are slower
Which construct defines a lightweight table inside a migration?
Base.metadata.reflect()
op.model()
sa.Session()
sa.table() with sa.column()

30. What is the alembic stamp command used for?

alembic stamp <rev> writes a revision ID into alembic_version without running any migration. It tells Alembic: 'treat this database as already at this revision'.

  • Adopting Alembic on a database that already has the schema: alembic stamp head after writing a baseline migration.
  • Recovering after a manual fix or a failed run left the table out of sync.
  • Marking a freshly built database (via create_all()) as current.
alembic stamp head
alembic stamp ae1027a6acf
alembic stamp --purge base

Use it carefully. Stamping the wrong revision makes Alembic skip or repeat migrations. --purge clears existing version rows first, which helps when the stored ID no longer exists.

Take quiz
What does alembic stamp head do?
Applies all pending migrations
Creates a new head revision
Deletes alembic_version
Records head in alembic_version without running migrations
When is stamp most useful?
When the schema already exists and Alembic must be told its revision
When creating an empty versions folder
When changing the database password
When renaming models

31. How do relative and partial revision identifiers work in Alembic?

Alembic accepts shortcuts so you rarely type a full 12-character ID.

Form Example Meaning
Partial ID ae10 Any unique prefix of a revision ID
Relative to current +2, -1 Two steps up, one step down from current
Relative to a revision ae10+1 One step after ae10
Symbolic head, base, heads Newest, empty start, all heads

alembic upgrade +2
alembic downgrade -1
alembic upgrade ae10+1

If a prefix matches more than one revision, Alembic reports it as ambiguous and you need to type more characters.

Take quiz
What does alembic downgrade -1 mean?
Go back one revision from the current one
Delete one file
Go back one table
Reduce the version number by 1 in the ini
What happens if a partial ID matches two revisions?
It picks the newest
Alembic reports it as ambiguous
It picks the oldest
It merges them

32. What happens when Alembic finds multiple head revisions?

Commands that target a single head stop with an error such as Multiple head revisions are present for given argument 'head'. Alembic cannot guess which branch should be the main line.

It usually happens when two developers each create a migration from the same parent on separate Git branches. After merging, the chain forks into two tips.

  • alembic heads lists the tips.
  • alembic upgrade heads applies every branch.
  • alembic merge heads -m "merge" joins them into one head.

Many teams add a CI check that fails when alembic heads returns more than one line, so the fork is caught before release.

Take quiz
What is the usual cause of multiple heads?
Running upgrade twice
Two migrations created from the same parent on different branches
Editing alembic.ini
Using too many tables
How can CI detect a forked history early?
Compare file sizes
Check the Mako template
Fail if alembic heads prints more than one revision
Count the tables

33. How do you merge multiple heads in Alembic?

Create a merge revision that lists all current heads as parents:

alembic merge heads -m "merge feature branches"
# or name them explicitly
alembic merge -m "merge" b7c2d4e8f901 a1b2c3d4e5f6

The new file has a tuple down_revision and usually empty upgrade() and downgrade(). After that, the history has a single head again and alembic upgrade head works.

revision = "c9d1e3f50a12"
down_revision = ("b7c2d4e8f901", "a1b2c3d4e5f6")

Before merging, check that the two branches do not touch the same table or column in conflicting ways. Alembic only joins the graph; it does not reconcile conflicting schema changes.

Take quiz
What does a merge revision's down_revision look like?
A single string 'merge'
None
A tuple of the parent revisions
A list of table names
Does alembic merge resolve conflicting schema changes?
Yes, it rewrites both migrations
Yes, it drops conflicting columns
Yes, it runs autogenerate
No, it only joins the revision graph

34. What is the difference between Alembic and Flask-Migrate?

Flask-Migrate is a thin Flask extension by Miguel Grinberg that wraps Alembic for Flask-SQLAlchemy projects. Alembic does the real work; Flask-Migrate wires it into Flask's app context and CLI.

Aspect Alembic Flask-Migrate
Scope Any SQLAlchemy project Flask + Flask-SQLAlchemy
Command alembic upgrade head flask db upgrade
Setup Edit env.py yourself Pre-configured env.py using the Flask app
Autogenerate alembic revision --autogenerate flask db migrate

Flask-Migrate reads the database URL and metadata from your Flask config, so you skip most of the manual env.py work. The migration files themselves are standard Alembic revisions.

Take quiz
What is Flask-Migrate in relation to Alembic?
A replacement engine
A separate migration format
A SQLAlchemy plugin for MySQL only
A Flask wrapper around Alembic
Which Flask-Migrate command corresponds to autogenerate?
flask db migrate
flask db create
flask db diff
flask db sync

35. How do you use Alembic with a FastAPI project?

FastAPI has no built-in migrations, so Alembic is added as a separate tool next to the app. It only needs your SQLAlchemy Base.metadata and a database URL.

  1. Run alembic init alembic in the project root.
  2. In env.py, import your Base and all model modules, and set target_metadata = Base.metadata.
  3. Load the URL from your settings object instead of alembic.ini.
  4. Generate and apply with alembic revision --autogenerate and alembic upgrade head.
from app.core.config import settings
from app.db.base import Base  # imports every model

config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
target_metadata = Base.metadata

If the app uses an async driver such as asyncpg, initialise with -t async. Run upgrade head as a deploy step, not inside the FastAPI startup event, so several workers do not race.

Take quiz
Where should the FastAPI database URL come from for Alembic?
The app settings, set into the config in env.py
The FastAPI route decorators
The OpenAPI schema
The uvicorn command line only
Why avoid running migrations in FastAPI's startup event?
Startup events cannot import Alembic
Multiple workers could run them at the same time
It disables autogenerate
It breaks Pydantic

36. What is the difference between Alembic and Django migrations?

Django migrations are built into Django's ORM and track app models automatically. Alembic is a standalone tool for SQLAlchemy, with more manual control.

Aspect Alembic Django migrations
ORM SQLAlchemy Django ORM
Create alembic revision --autogenerate makemigrations
Apply alembic upgrade head migrate
History table One alembic_version row (per head) django_migrations, one row per applied migration
Ordering down_revision links dependencies lists, per app
Renames Detected only by hand Can ask whether a field was renamed

Django keeps a per-app history and builds model state from the migration files. Alembic keeps one global chain, which is simpler but gives you more room to make mistakes in env.py.

Take quiz
Which table records applied migrations in Django?
alembic_version
django_migrations
schema_history
migrate_log
Which ORM does Alembic target?
Django ORM
Peewee only
SQLAlchemy
Pony ORM only

37. Why should you use a naming convention with Alembic?

Constraints you do not name get database-generated names, such as users_email_key on PostgreSQL, and those differ per backend. Dropping or altering them later then needs guesswork. A naming_convention on MetaData gives every constraint a predictable name.

from sqlalchemy import MetaData
convention = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}
Base = declarative_base(metadata=MetaData(naming_convention=convention))

Autogenerate then writes explicit names into migrations, op.drop_constraint() works reliably, and SQLite batch mode can find constraints. Set it before your first migration; changing it later means renaming existing constraints.

Take quiz
What does naming_convention on MetaData provide?
Faster queries
Automatic table renames
Predictable names for constraints and indexes
A separate alembic_version table
What is the best time to introduce a naming convention?
After the first production incident
Only when using SQLite
Never, Alembic names everything
Before the first migration

38. How do you rename a column in Alembic without losing data?

Write the rename by hand with op.alter_column(..., new_column_name=...). Autogenerate would instead emit a drop and add, which destroys the data in that column.

def upgrade():
    op.alter_column(
        "users", "fullname",
        new_column_name="full_name",
        existing_type=sa.String(100),
    )

def downgrade():
    op.alter_column(
        "users", "full_name",
        new_column_name="fullname",
        existing_type=sa.String(100),
    )

MySQL needs existing_type because its syntax re-declares the column. For a live system with older app versions still running, use expand and contract: add the new column, copy data, switch the code, then drop the old column in a later release.

Take quiz
Why does autogenerate risk data loss on a rename?
It truncates the table
It locks the database
It changes the primary key
It generates a drop and an add
Which argument of op.alter_column renames a column?
new_column_name
rename_to
target_name
column_alias

39. How do compare_type and compare_server_default affect autogenerate?

Both are context.configure() options that tell autogenerate to also look at column types and server defaults, not just presence of columns.

Option Detects Default
compare_type Type changes such as String(50) to String(100) On in current versions
compare_server_default Changes to server_default Off

context.configure(
    connection=connection,
    target_metadata=target_metadata,
    compare_type=True,
    compare_server_default=True,
)

Server default comparison can produce false positives because databases normalise default expressions, for example now() versus CURRENT_TIMESTAMP. Review the output carefully or supply a custom comparison function.

Turning on compare_type is a good habit for new projects, since silent type drift between models and the database otherwise goes unnoticed until a query fails.

Take quiz
Which option makes autogenerate look at server_default changes?
compare_server_default
compare_defaults_only
detect_defaults
server_default_check
Why can server default comparison give false positives?
Alembic ignores defaults
Databases rewrite default expressions into a normalised form
Defaults are encrypted
Python cannot parse SQL

40. How do you exclude tables from autogenerate using include_object?

Pass an include_object function to context.configure(). Alembic calls it for each table, column, index and constraint, and anything for which it returns False is skipped.

def include_object(obj, name, type_, reflected, compare_to):
    if type_ == "table" and name in {"spatial_ref_sys", "celery_taskmeta"}:
        return False
    return True

context.configure(
    connection=connection,
    target_metadata=target_metadata,
    include_object=include_object,
)

This is useful for tables owned by extensions or other services, which would otherwise be proposed for dropping because they are not in your metadata. A lighter option is include_name, which filters by name before reflection.

Take quiz
What does include_object returning False do?
The object is dropped
The object is skipped by autogenerate
Alembic stops with an error
The object is renamed
Why exclude tables like spatial_ref_sys?
They are read-only for Alembic
They break Mako
They exist in the database but not in your models
They use a different driver

41. How do you manage migrations for multiple databases with Alembic?

There are two common patterns. The built-in multidb template keeps one revision chain for several databases; separate configs keep one chain per database.

  • multidb template: alembic init -t multidb, with databases = engine1, engine2 in alembic.ini. Revisions contain upgrade_engine1(), upgrade_engine2() and so on.
  • Separate environments: one folder and ini per database, selected with alembic -c db2.ini upgrade head, or sections picked with -n.

Use the multidb template when the databases always move together. Choose separate chains when they are owned by different services, because their release cycles will differ.

Take quiz
Which init template targets several databases in one chain?
cluster
shared
multidb
federated
Which option selects a different ini file?
-i
--conf-path
-f
-c

42. What is the difference between Alembic and Flyway or Liquibase?

All three apply versioned schema changes, but they come from different ecosystems. Flyway and Liquibase are JVM tools built around SQL or XML/YAML changelogs. Alembic is Python-first and ties into SQLAlchemy.

Aspect Alembic Flyway Liquibase
Language Python scripts Plain SQL or Java XML, YAML, JSON or SQL
Autogenerate Yes, from SQLAlchemy models No (paid diff features) Yes, diff and generateChangeLog
Ordering Revision graph, supports branches Version numbers Changelog order
Integrity check No checksum Checksum validation MD5 checksums

Pick Alembic when your models are in SQLAlchemy and you want migrations in the same language. Pick Flyway or Liquibase for polyglot teams or when SQL is the source of truth.

Take quiz
Which of these tools does not verify migration checksums?
Flyway
Liquibase
Both Flyway and Liquibase
Alembic
Which language are Alembic migrations written in?
Python
XML only
Java only
Plain SQL files only

43. Explain the execution flow of alembic upgrade head?

alembic upgrade head loads the config, reads the revision scripts, runs env.py, and applies each pending revision inside a transaction when the database supports it.

sequenceDiagram
    participant CLI as alembic CLI
    participant SD as ScriptDirectory
    participant ENV as env.py
    participant DB as Database
    CLI->>SD: read alembic.ini, load versions/
    SD-->>CLI: revision graph
    CLI->>ENV: run env.py
    ENV->>DB: connect, begin transaction
    DB-->>ENV: current rev from alembic_version
    loop each pending revision
        ENV->>DB: run upgrade()
        ENV->>DB: update alembic_version
    end
    ENV->>DB: commit
  1. The CLI parses alembic.ini and resolves script_location.
  2. ScriptDirectory reads files in versions/ and builds the revision map from down_revision links.
  3. env.py runs, creates the engine and calls context.configure().
  4. The MigrationContext reads the current revision and computes the path to the target.
  5. Each upgrade() runs and the version row is updated.
  6. The transaction commits.

A failure rolls back the whole run on PostgreSQL. On MySQL, DDL commits implicitly, so earlier steps stay applied.

Take quiz
Which component builds the revision graph from the versions folder?
ScriptDirectory
MigrationContext only
The Mako renderer
SQLAlchemy Inspector
What updates alembic_version during upgrade?
The database trigger
Alembic, after each revision's upgrade() runs
The app on restart
The Mako template

44. Explain the internal working of Alembic autogenerate?

Autogenerate is a pipeline: reflect, compare, convert, render. It lives in alembic.autogenerate and the public entry point for the comparison step is compare_metadata().

flowchart LR
    A["Models: target_metadata"] --> C[compare_metadata]
    B["Live DB: Inspector reflection"] --> C
    C --> D["Diff tuples: add_table, remove_column ..."]
    D --> E["MigrationScript directives"]
    E --> F["Render to Python op calls"]
    F --> G["New revision file"]
  1. SQLAlchemy's Inspector reflects tables, columns, indexes and foreign keys from the database.
  2. Alembic compares them with target_metadata and produces diff tuples such as ('add_column', ...).
  3. The diffs become directive objects (UpgradeOps, DowngradeOps).
  4. Renderers turn directives into op.* source code, which is placed into script.py.mako.

You can modify the directives before rendering with process_revision_directives, for example to skip empty migrations:

def process_revision_directives(context, revision, directives):
    script = directives[0]
    if script.upgrade_ops.is_empty():
        directives[:] = []

Take quiz
Which SQLAlchemy component reflects the live schema for autogenerate?
Session
Inspector
Mapper
Dialect compiler
Which hook lets you change the generated directives before they are written?
post_render_hook
pre_commit_script
process_revision_directives
edit_revision

45. How do branch labels and depends_on work in Alembic?

Alembic's history is a graph, not just a line. Branch labels name independent lines of migrations, and depends_on adds a cross-branch ordering rule without making one the parent of the other.

alembic revision -m "plugin base" --head=base --branch-label=plugin_a
alembic upgrade plugin_a@head
alembic revision -m "needs core" --head=plugin_a@head --depends-on=ae1027a6acf

  • --head=base starts a new, separate branch.
  • --branch-label gives it a name you can use as label@head.
  • depends_on = "ae1027a6acf" inside a revision says it must run after that revision, even in another branch.

This suits plugin-style projects where each module owns its tables but may depend on a core schema. Use version_locations to store each branch's files in its own folder.

Take quiz
What does depends_on add to a revision?
A second database URL
A rollback script
An ordering dependency on a revision in another branch
A new head
Which syntax upgrades only a labelled branch?
alembic upgrade --branch plugin_a
alembic upgrade plugin_a:only
alembic upgrade plugin_a.head
alembic upgrade plugin_a@head

46. How do you run Alembic with an async SQLAlchemy engine?

Initialise with the async template, which provides an env.py that creates an AsyncEngine and runs the sync migration code inside run_sync.

alembic init -t async alembic

from sqlalchemy.ext.asyncio import async_engine_from_config
import asyncio

def do_run_migrations(connection):
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()

async def run_async_migrations():
    engine = async_engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
    )
    async with engine.connect() as conn:
        await conn.run_sync(do_run_migrations)
    await engine.dispose()

asyncio.run(run_async_migrations())

The URL must use an async driver, for example postgresql+asyncpg://. The migration scripts themselves stay synchronous, since Alembic's op calls are not coroutines.

Take quiz
Which template sets up env.py for an async engine?
asyncio-lite
coroutine
aio
async
Are individual migration scripts written with await?
No, they stay synchronous and run via run_sync
Yes, every op call is awaited
Yes, but only upgrade()
Only downgrade() is async

47. How do you troubleshoot "Can't locate revision identified by" error?

This error means the database's alembic_version holds an ID that Alembic cannot find in the versions/ folder. The database and the code are out of step.

  1. Run alembic current and note the ID.
  2. Search versions/ for it. A missing file is the usual cause: a deleted or unmerged migration, or a branch not checked out.
  3. Run alembic heads and alembic history to see what the code knows.
  4. Confirm you are pointed at the right database; staging and production IDs get mixed up often.

Fixes depend on the cause. Restore or merge the missing file if the revision was real. If the ID is truly stale, correct the pointer:

alembic stamp --purge head   # only if the schema really matches head

Never stamp blindly. Compare the actual schema first, or you will skip migrations.

Take quiz
What is the most common cause of this error?
The revision file for the stored ID is missing from versions/
alembic.ini has a typo in logging
Mako is not installed
Too many revisions exist
What should you do before running alembic stamp as a fix?
Delete alembic_version
Confirm the real schema matches the revision you stamp
Run downgrade base
Regenerate every revision

48. How can you avoid locking large tables with Alembic on PostgreSQL?

Long locks come from DDL that rewrites a table or waits behind running queries. Plan each operation so it holds the lock only briefly.

  • Indexes: use CREATE INDEX CONCURRENTLY, which cannot run inside a transaction.
  • Lock timeout: set lock_timeout so the migration fails fast instead of blocking traffic.
  • Backfills: update in small batches, not one huge statement.
  • Constraints: add as NOT VALID and VALIDATE later.
def upgrade():
    op.execute("SET lock_timeout = '5s'")
    with op.get_context().autocommit_block():
        op.create_index(
            "ix_orders_created_at", "orders", ["created_at"],
            postgresql_concurrently=True,
        )

autocommit_block() pauses the migration transaction so the concurrent index build is allowed. Adding a nullable column without a default is a quick, metadata-only change.

Take quiz
Why use autocommit_block for CREATE INDEX CONCURRENTLY?
It disables Alembic
That statement cannot run inside a transaction
It speeds up SELECT
It avoids revision IDs
What does lock_timeout achieve in a migration?
It speeds up the backfill
It locks more tables
The migration fails quickly rather than blocking traffic for a long time
It skips constraints

49. How do you run Alembic migrations safely in CI/CD?

Treat migrations as a controlled release step: validate them on every pull request, then apply them once per deploy, before the new code starts.

  1. Check one head: fail if alembic heads lists more than one.
  2. Check drift: alembic check fails if models and migrations disagree.
  3. Test the chain: alembic upgrade head on an empty database, plus a downgrade test where it matters.
  4. Deploy: run alembic upgrade head as a single job, not on every app instance.
  5. Back up before risky changes in production.

Write migrations to be backward compatible (expand then contract), so the old app version keeps working while the new schema rolls out. For approval workflows, generate SQL with --sql and attach it to the release.

Take quiz
Which command fails when models and migrations differ?
alembic verify-heads
alembic diff-only
alembic check
alembic lint
Where should upgrade head run in a deploy?
In every app instance at startup
In a cron job at midnight
Inside each HTTP request
As one dedicated job before the new app version starts

50. What is transactional DDL and how does Alembic handle it?

Transactional DDL means schema changes such as CREATE TABLE can be rolled back like normal data changes. PostgreSQL supports it, so a failed migration leaves no partial changes. MySQL commits DDL implicitly, so a failure can leave the schema half-migrated.

Database Transactional DDL Effect of failure mid-migration
PostgreSQL Yes Whole run rolls back
SQLite Mostly Usually rolls back
MySQL / MariaDB No Earlier statements stay applied
Oracle No Earlier statements stay applied

Alembic checks the dialect's transactional_ddl flag and wraps the run in one transaction when it is supported. By default all pending revisions share a single transaction. Set transaction_per_migration=True in context.configure() to commit after each revision instead.

On non-transactional databases, keep each migration small, and be ready to fix state with alembic stamp.

Take quiz
What does transactional DDL allow?
Faster index builds
Skipping alembic_version
Running without a connection
Schema changes to be rolled back with the transaction
Which option commits after each revision instead of once per run?
transaction_per_migration=True
commit_each=True
autocommit_all=True
split_transactions=1
«
»

Comments & Discussions