# Migration Guide Design §31-§32. Flyway owns the schema; Hibernate only validates. ## Who may change the schema | Environment | Mode | |---|---| | local, test, dev | migrate at startup with the migration credential | | staging, prod | deployment-owned migration; the application validates only | Migrating from inside the application in production means every instance of a rolling deploy races to apply the same script, and the loser's failure is indistinguishable from a real one. `ddl-auto` is `validate` or `none`. Never `update`: it never drops or narrows anything, so it produces a schema that is neither the old one nor the one the migrations describe — silently, on whichever instance started first. ## Validation fails closed and never repairs `FlywayValidationGate` throws `SchemaMismatchException` on a checksum mismatch, a missing migration, or a schema Hibernate disagrees with. It never calls `repair`. Repair rewrites the schema history table to match whatever scripts are on disk. That resolves the symptom by deleting the evidence: a checksum mismatch means the deployed script differs from the applied one, and the interesting question is which change is missing from this database. Repair makes that question unaskable. It exists only as an explicit admin operation with an operator, a reason, and an approval (design §8.4). Only Flyway's structured error codes reach the exception. Its messages embed the script path and part of the failing statement. ## Concurrent index builds `CREATE INDEX CONCURRENTLY` cannot run inside a transaction block, and Flyway wraps migrations in one by default. The migration therefore needs a companion configuration: ```conf # V42__order_index.sql.conf executeInTransaction=false ``` `ConcurrentIndexMigrationInspector` fails validation without it, and additionally requires the migration to contain nothing else. A failed concurrent build leaves an invalid index behind; recovering is a single `DROP INDEX` when the migration did nothing else, and a manual reconstruction of partial state when it did. An invalid index is not merely useless — the planner ignores it while every write still maintains it. `FailedConcurrentIndexRecovery` reports them with the statement to run, and deliberately does not drop them: an invalid index can also mean a build is still running, and the two are indistinguishable from the catalog alone. ## Upgrade scenarios Three, each catching something the others do not: | Scenario | Catches | |---|---| | `empty` | an early migration edited to match a later one, no longer applying to a fresh database | | `previous-release` | the actual deployment path; the only one exercising this release's migrations | | `oldest-supported` | a migration that silently assumes state only recent databases have | Each asserts a data invariant, not just the schema version. A migration that renames a column and loses its contents leaves the version correct and the data gone.