AccelerUpDocs
Running AccelerUp/Operations/Database migrations

Database migrations

How schema changes are written, applied and ordered — and the two rules that keep rollbacks possible.

Migrations live in one file: backend/internal/repository/postgres/migrate.go, as a slice of {version, sql} pairs.

Around 257 migrations at the time of writing.

How they run

On start, the backend:

  1. Creates schema_migrations if it does not exist.
  2. Sorts the slice by version — ascending, regardless of where the entry sits in the file.
  3. Skips any version already recorded.
  4. Applies the rest, recording each.
Note

The sort is why a migration added in the middle of the file still runs last. Position in the source does not decide order; the version prefix does. Version prefixes are zero-padded so a lexicographic sort equals the intended numeric order.

An advisory lock is taken around the whole run, so two replicas starting together do not race.

Adding one

go
{"258_thing_that_changed", `
-- ── Why this table exists ──────────────────────────────────────────
--
-- The reason, in prose, for whoever reads this in a year.
CREATE TABLE IF NOT EXISTS thing (
    org_id     UUID        NOT NULL,
    ...
    PRIMARY KEY (org_id, ...)
);
CREATE INDEX IF NOT EXISTS idx_thing_q ON thing(org_id, bucket);
`},
Caution

The SQL is a Go raw string literal. A backtick anywhere inside it ends the literal and breaks the build in a way whose error message points somewhere else entirely. Never put a backtick in migration SQL.

The two rules

1. Backwards-compatible for one release

During a rolling deploy both versions are serving. The old code must work against the new schema for the length of the rollout.

  • Adding a column: fine. Give it a default.
  • Adding a table or index: fine.
  • Dropping or renaming a column: two releases. Add the new one and start writing both; migrate; remove the old one next release.

This is also what makes a rollback within a release always safe.

2. Idempotent

IF NOT EXISTS everywhere. A migration that has partially applied — the process died halfway — must be safe to run again.

JSONB instead of a column

Several features store structured settings as JSONB. Adding a field to such a structure needs no migration: the Go struct is marshalled whole.

This is why, for example, clock alignment was added to SQL alert schedules with no schema change — schedule was already JSONB.

Use this deliberately, not as a habit. A field you will want to query or index belongs in a column.

Checking what has run

sql
SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 10;

Retention

Migrations create tables; the cleanup worker prunes them. A new high-volume table needs an entry in the cleanup worker's retention list, or it grows forever. See Data retention.

Where this behaviour lives: backend/internal/repository/postgres/migrate.go. If the code and this page disagree, the code is right — please fix the page.

Part of Running AccelerUpOperating the platform itself: architecture, deploys, backups.