diff --git a/develop-docs/backend/application-domains/database-migrations/index.mdx b/develop-docs/backend/application-domains/database-migrations/index.mdx index dd8aa71ed57b5..e0392167ea5b0 100644 --- a/develop-docs/backend/application-domains/database-migrations/index.mdx +++ b/develop-docs/backend/application-domains/database-migrations/index.mdx @@ -51,6 +51,8 @@ You can also generate an empty migration with `sentry django makemigrations None: + pass # there must be at least one migrations tests or the suite fails +``` + +It's also worth capturing a schema snapshot before you squash, so you can confirm the squash didn't change anything: + +```bash +make reset-db # clean state at the latest migration +docker exec -i postgres-postgres-1 pg_dump -U postgres -d sentry --schema-only -x -O > before_schema.sql +``` + +## Run the squash tool + +```bash +python tools/migrations/squash.py +``` + +This reads `migrations_lockfile.txt` and, for every app that isn't already squashed, generates one new migration (`0001_squashed_.py`) that replaces its entire history. It sets `is_post_deployment = True` on the new file, populates its `replaces` list with the names of every migration it's replacing, and rewrites `migrations_lockfile.txt` to point at the new squashed migrations. + +After running it, restore the old, now-superseded migration files (for example with `git checkout -- ` for anything the tool deleted) so they land in the same commit alongside the new squashed files. Deleting them right away would mean self-hosted installs that haven't yet applied those individual migrations would have no way to apply them — the [hard-stop check](#update-the-self-hosted-hard-stop) below is what actually makes it safe to remove them later. Django doesn't care that the replaced files still exist on disk; `replaces` only refers to them by name. + +## Verify the schema didn't change + +Reset the database again and dump the schema, this time from the squashed migrations: + +```bash +make reset-db +docker exec -i postgres-postgres-1 pg_dump -U postgres -d sentry --schema-only -x -O > after_schema.sql +``` + +```bash +diff -u before_schema.sql after_schema.sql +``` + +The two should be equivalent. Some fields may appear in a different order — this comes from Postgres's internal system catalogs and isn't something the squash controls — so inspect any diff manually rather than assuming it's a real regression. + +## Update the self-hosted hard stop + +Squashing means Django will no longer have the individual migration files to fall back on, so self-hosted installs that are too far behind need to be blocked from skipping straight past the squash. This is enforced by the `migration_heads` tuple inside `_check_history()` in `src/sentry/runner/commands/upgrade.py`: + +```python +migration_heads = ( + "1118_add_group_derived_data", # pre-squash + "0001_squashed_1118_add_group_derived_data", # post-squash +) +``` + +Update this tuple by hand to the last pre-squash migration name and the new squashed migration name for the app you squashed. `sentry upgrade` aborts with a "you've skipped a hard stop" error unless at least one of these two names shows up as already applied in the install's `django_migrations` table — so an install that applied the old migrations individually, and a fresh install that only ever sees the squashed file, both pass. + +Ship this as a normal self-hosted release, and add it to the [Hard Stops list](/self-hosted/releases/#hard-stops) so people upgrading from further behind know they need to pass through it. + +## Wait for a release, then clean up + +Once the release with the updated `migration_heads` has shipped, give it at least one more monthly self-hosted release cycle so installs have had a chance to upgrade through the hard stop. After that, the old individual migration files you kept around are no longer needed by anyone, and can be deleted in a follow-up PR. + +At that point you can also, per [Django's own guidance on squashing](https://docs.djangoproject.com/en/stable/topics/migrations/#migration-squashing), remove the `replaces` attribute from the squashed migration and let it become a regular numbered migration. Sentry hasn't needed to do this yet, but it avoids indefinitely nesting `replaces` lists across future squashes of the same app.