Files
spring-boot-demo/db-migrations-flyway-liquibase/docs/02-anatomy-of-a-migration-run.md
T
asmhatreandClaude Sonnet 5 3908331431 Add db-migrations-flyway-liquibase: Flyway vs Liquibase migrations, rollbacks and baselines on Spring Boot 4.1
Companion code for the Flyway vs Liquibase article: checksum validation, out-of-order
and repeatable migrations, baselining an existing schema, Flyway Community's undo/diff/deploy
stubs, concurrent-startup locking for both tools, Liquibase changeset identity and rollback
(auto-generated vs explicit), a verified Liquibase 5.0.3 filename-caching defect, the new
OSS license service, the FSL license change, and running both tools against one database.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
2026-09-15 07:08:57 +00:00

6.1 KiB

2. Anatomy of a Flyway migration run

← 1. The problem and mental model · Index · Next: 3. Checksum validation →

The smallest possible Flyway setup is two SQL files and nothing else. This module's are V1__create_customer.sql and V2__seed_customer.sql, exercised by FlywayHappyPathTest. The filename is the metadata: V for versioned, 1 or 2 as the version, two underscores, then a description Flyway derives straight from the rest of the filename.

create table customer (
    id bigint generated by default as identity primary key,
    name varchar(120) not null,
    email varchar(200) not null unique
);

That's the whole of V1. After both files run once, flyway_schema_history looks like this (quoted verbatim from docs/output/01-flyway-happy-path.txt):

installed_rank | version | description                               | type  | checksum    | success
---------------+---------+-------------------------------------------+-------+-------------+--------
-1             | NULL    | << Flyway Schema History table created >> | TABLE | NULL        | true
1              | 1       | create customer                           | SQL   | 1461549807  | true
2              | 2       | seed customer                              | SQL   | -1214875726 | true

Two things worth noticing, both easy to get wrong if you only skim the docs:

  • The description column drops underscores for spaces. The file is V1__create_customer.sql; the row says create customer, not create_customer. This module's own test assertions got this wrong on the first pass — they were written expecting the underscore to survive, and the real output corrected them.
  • checksum is a signed 32-bit CRC of the file's content, not a hash of the SQL statements Flyway ran. Chapter 3 is entirely about what happens when that number stops matching.
V1__create_customer.sql V2__seed_customer.sql Flyway.migrate() for each file, in version order: already in history? skip. else: run it, record a row. flyway_schema_history rank -1 TABLE rank 1 V1 SQL rank 2 V2 SQL one row per migration, named lowercase, quoted

A case-folding trap worth knowing before you write your own query

That last line in the diagram — "named lowercase, quoted" — is not decoration. Flyway creates and queries its own table using quoted, lowercase identifiers: "flyway_schema_history", "installed_rank", and so on. H2, like most databases, folds unquoted identifiers to uppercase by default. So this innocent-looking query, run from your own code against the exact same database, finds nothing:

select installed_rank from flyway_schema_history   -- looks for FLYWAY_SCHEMA_HISTORY — not found

The fix is to quote it the same way Flyway does:

select "installed_rank" from "flyway_schema_history"

This module's own MigrationDiagnosticsController hit exactly this while it was being written — the first version queried Flyway's table unquoted and silently got "table not found" back. Every test in this module that reads flyway_schema_history directly (see FlywayHappyPathTest and the rest of the flyway package) quotes it for exactly this reason. Liquibase's own tables don't need this — its unquoted DDL for DATABASECHANGELOG folds consistently with H2's default, so plain, unquoted select ... from databasechangelog works fine (see LiquibaseHappyPathTest in chapter 9). The asymmetry is real, verified by running both, and it is exactly the kind of thing that costs you twenty minutes the first time you write a raw report query against a Flyway-managed schema.

Going deeper

  • Flyway migration naming — the full naming grammar, including undo and repeatable prefixes (rel="nofollow").
  • H2's identifier case sensitivity is documented on the H2 SQL grammar page under DATABASE_TO_LOWER/DATABASE_TO_UPPER (rel="nofollow") — this module doesn't set either, so it runs on H2's default.