Flyway / Migrations — Practical
Flyway / Migrations — Practical patterns
Section titled “Flyway / Migrations — Practical patterns”Project structure
Section titled “Project structure”db/ migrations/ V1__init.sql V2__users_table.sql V3__orders_table.sql V4__add_user_email_index.sql R__view_user_summary.sql conf/ flyway.dev.conf flyway.prod.confflyway.conf
Section titled “flyway.conf”flyway.url=jdbc:postgresql://db:5432/appflyway.user=appflyway.password=${DB_PASSWORD}flyway.locations=filesystem:db/migrationsflyway.cleanDisabled=trueflyway.validateOnMigrate=trueflyway.outOfOrder=falseflyway.baselineOnMigrate=trueflyway.baselineVersion=1Online migration patterns
Section titled “Online migration patterns”Add nullable, backfill, set NOT NULL
Section titled “Add nullable, backfill, set NOT NULL”-- V42__add_user_status_step1.sqlALTER TABLE users ADD COLUMN status TEXT;-- backfill via app or Job (batched)UPDATE users SET status = 'active' WHERE status IS NULL AND id BETWEEN 0 AND 100000;-- V44__user_status_not_null.sqlALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';ALTER TABLE users ALTER COLUMN status SET NOT NULL;Index without locking
Section titled “Index without locking”-- V45__email_index.sqlCREATE INDEX CONCURRENTLY idx_users_email ON users(email);CONCURRENTLY can’t run in transaction → put in own migration. Configure flyway: flyway.mixed=true if needed.
Rename column (multi-release)
Section titled “Rename column (multi-release)”-- V50: add and copyALTER TABLE users ADD COLUMN full_name TEXT;-- backfill in code-- V60 (next release): drop old after code is updatedALTER TABLE users DROP COLUMN name;Repeatable migration
Section titled “Repeatable migration”-- R__user_summary_view.sqlDROP VIEW IF EXISTS user_summary;CREATE VIEW user_summary ASSELECT u.id, count(o.id) AS order_countFROM users u LEFT JOIN orders o ON o.user_id = u.idGROUP BY u.id;Migration as Kubernetes Job
Section titled “Migration as Kubernetes Job”apiVersion: batch/v1kind: Jobmetadata: { name: db-migrate-${GIT_SHA} }spec: ttlSecondsAfterFinished: 600 backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: flyway image: flyway/flyway:10 args: ["migrate"] env: - name: FLYWAY_URL value: "jdbc:postgresql://db:5432/app" - name: FLYWAY_USER value: app - name: FLYWAY_PASSWORD valueFrom: { secretKeyRef: { name: db, key: password } } volumeMounts: [{ name: migrations, mountPath: /flyway/sql }] volumes: - name: migrations configMap: { name: db-migrations }Argo CD PreSync hook or helm install --wait waits before deploying app pods.
Test in CI
Section titled “Test in CI”- run: | docker run -d --name pg -e POSTGRES_PASSWORD=pw -p 5432:5432 postgres:16 sleep 3 docker run --rm --network=host \ -v $PWD/db/migrations:/flyway/sql \ flyway/flyway:10 \ -url=jdbc:postgresql://localhost:5432/postgres \ -user=postgres -password=pw \ migrateBackfill helper (Node)
Section titled “Backfill helper (Node)”async function backfillStatus(batch = 1000) { while (true) { const r = await db.query(` WITH cte AS ( SELECT id FROM users WHERE status IS NULL ORDER BY id LIMIT $1 FOR UPDATE SKIP LOCKED ) UPDATE users SET status='active' WHERE id IN (SELECT id FROM cte) RETURNING 1 `, [batch]); if (r.rowCount === 0) break; await new Promise(r => setTimeout(r, 100)); }}Useful tools
Section titled “Useful tools”- Flyway Desktop — UI.
- Atlas — declarative, diff-based, lint.
- gh-ost / pt-osc (MySQL) — online schema change.
- pgroll (PostgreSQL) — versioned online migrations.
- squawk — PG migration linter.
- schemahero — K8s operator for declarative schema.