Databases 16 min read

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.

Hacker Afternoon Tea
Hacker Afternoon Tea
Hacker Afternoon Tea
Sentry Developer Contribution Guide – Database Migrations

Commands

All commands are executed from the getsentry repository; replace getsentry with sentry when appropriate.

Upgrade database to latest

sentry upgrade

runs 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 0005

Generate 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 0003

Generate 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> --empty

Merge 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 work

Combining .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)"
    )
]
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

SQLbackend developmentDjangoSentryPostgreSQLdatabase migrations
Hacker Afternoon Tea
Written by

Hacker Afternoon Tea

You might find something interesting here ^_^

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.