Sentry Developer Contribution Guide – Database Migrations
This guide explains how Sentry uses Django migrations to manage database schema changes, covering commands for upgrading, targeting specific migrations, generating SQL, creating and merging migrations, and detailed best‑practice procedures for handling filters, indexes, column and table modifications, foreign keys, and renaming while avoiding downtime.
Commands
All commands are executed from the getsentry repository; replace getsentry with sentry when appropriate.
Upgrade database to latest
sentry upgraderuns the migration process automatically. The same effect can be achieved with sentry django migrate.
Move database to a specific migration
Useful for testing or rolling back. Syntax:
sentry django migrate <app_name> <migration_name>The migration_name may be partially matched (usually the numeric prefix). Example:
sentry django migrate sentry 0005Generate SQL for a migration
Shows the exact SQL that Django will execute, aiding code review.
sentry django sqlmigrate <app_name> <migration_name>Example:
sentry django sqlmigrate sentry 0003Generate migrations
Creates migration files from model changes. sentry django makemigrations For a single app: sentry django makemigrations <app_name> Empty migrations (useful for data migrations) can be generated with:
sentry django makemigrations <app_name> --emptyMerge migrations into master
When merging to master, a conflict may appear in migrations_lockfile.txt. This file prevents two migrations with the same number from being merged; a conflict usually indicates that another migration was submitted earlier.
Guidelines
Filters
For large or unindexed tables, iterate over the whole table instead of using .filter() to avoid loading many rows into memory. Example of a direct filter (not recommended for huge tables):
EnvironmentProject.objects.filter(environment__name="none")Preferred batch‑processing pattern:
for env in RangeQuerySetWrapperWithProgressBar(EnvironmentProject.objects.all()):
if env.name == 'none':
# perform required workCombining .filter() with RangeQuerySetWrapperWithProgressBar is avoided because the wrapper sorts by id, bypassing indexes and causing a slower but more evenly distributed load.
Indexes
Use CREATE INDEX CONCURRENTLY on large tables. Since this cannot run inside a transaction, set atomic = False in the migration.
Delete column / table
Because migrations run before the new code is fully rolled out, deleting a column or table directly can cause runtime errors. Follow a multi‑step process:
Mark the column as nullable and create a migration.
Deploy.
Remove the column from the model while keeping the migration state as removed.
Deploy.
Create a migration that actually drops the column.
Deploy.
Example migration that removes fields without touching the database:
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.RemoveField(model_name="alertrule", name="alert_threshold"),
migrations.RemoveField(model_name="alertrule", name="resolve_threshold"),
migrations.RemoveField(model_name="alertrule", name="threshold_type"),
],
)
]After deployment, a separate migration runs the actual ALTER TABLE … DROP COLUMN statements.
Rename table
Renaming a table in PostgreSQL is risky because old code may still reference the original name during deployment. Preferred approach: rename the Django model and set Meta.db_table to the existing table name, avoiding any database‑level rename.
If a physical rename is unavoidable, the steps are:
Create a new table with the new name.
Write to both old and new tables (ideally within a transaction).
Back‑fill old rows into the new table.
Switch the model to read from the new table.
Stop writing to the old table and remove references.
Drop the old table.
Add column
Always add new columns as nullable. This avoids a full table rewrite and prevents deployment‑time failures when older code inserts rows without the new column.
Add NOT NULL constraint
Adding NOT NULL can lock the table. Safer approaches:
Create the constraint as NOT VALID first, then validate it later. This holds only a SHARE UPDATE EXCLUSIVE lock, causing minimal performance impact (≈0.5‑1%). Example:
ALTER TABLE tbl ADD CONSTRAINT cnstr CHECK (col IS NOT NULL) NOT VALID;
ALTER TABLE tbl VALIDATE CONSTRAINT cnstr;If the table is small (a few million rows or less), a direct NOT NULL may be acceptable.
Add column with default value
Adding a column with a default rewrites the whole table. Preferred method: add a nullable column in PostgreSQL, set the default in Django, and back‑fill existing rows with a data migration.
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.AddField(
model_name="mymodel",
name="new_field",
field=models.PositiveSmallIntegerField(null=True),
),
],
state_operations=[
migrations.AddField(
model_name="mymodel",
name="new_field",
field=models.PositiveSmallIntegerField(null=True, default=1),
),
],
)
]Change column type
Changing a column type usually requires a full table rewrite and is risky. Safe exceptions include expanding varchar size, converting varchar to text, or increasing numeric precision while keeping scale.
For other types, use the following pattern:
Create a new column with the desired type.
Write to both old and new columns.
Back‑fill and convert old values.
Update code to use the new column.
Stop writing to the old column and remove references.
Drop the old column.
Rename column
Renaming a column in PostgreSQL suffers the same deployment risk as table renames. Preferred method: rename the field in Django and set db_column to the existing column name, avoiding any database‑level rename.
If a rename is required, follow the same multi‑step process used for changing column types.
Foreign keys
Creating foreign keys on very busy tables (e.g., Project, Group) can cause lock contention. Use Django‑level foreign keys without database constraints by setting db_constraint=False on the field definition.
Delete table
When a table is referenced by foreign keys, first remove the constraints by setting db_constraint=False on the referencing fields, deploy, delete the model and its references in code (state‑only migration), deploy again, then create a migration that drops the table (often using an empty migration with a RunSQL operation).
Example of dropping a table with a reversible empty migration:
operations = [
migrations.RunSQL(
"""
DROP TABLE \"sentry_alertruletriggeraction\";
""",
reverse_sql="CREATE TABLE sentry_alertruletriggeraction (fake_col int)"
)
]Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
