db-migrations-flyway-liquibase: fix dead Redgate/Liquibase doc links and back the lock-defaults claim with a real transcript

Several documentation.red-gate.com and liquibase.com URLs added in the previous commit had
since moved (Redgate restructured its docs under /flyway/reference/, Liquibase Pro pricing
moved to /pricing); this repoints them at the current, verified-200 pages. Also captures
docs/output/16-liquibase-lock-defaults-javap.txt, the trimmed javap output backing chapter
11's claim about Liquibase's default lock-poll and lock-wait settings, which chapter 11
previously asserted without a committed artifact to back it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
This commit is contained in:
2026-09-15 07:31:49 +00:00
co-authored by Claude Sonnet 5
parent 3908331431
commit 67e1b2d8a9
12 changed files with 48 additions and 13 deletions
+6
View File
@@ -87,6 +87,12 @@ helper — the tests assert the same numbers they print, so a transcript going s
| [`14-liquibase-oss-license-service.txt`](docs/output/14-liquibase-oss-license-service.txt) | [`LiquibaseLicenseServiceTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseLicenseServiceTest.java) |
| [`15-both-together-same-datasource.txt`](docs/output/15-both-together-same-datasource.txt) | [`BothTogetherTest`](src/test/java/com/ankurm/dbmigrations/BothTogetherTest.java) |
One file is the exception: [`16-liquibase-lock-defaults-javap.txt`](docs/output/16-liquibase-lock-defaults-javap.txt)
isn't produced by `mvn test` — it's the trimmed `javap -p -c -constants` output confirming
Liquibase's default lock-poll and lock-wait settings straight from `liquibase-core`'s bytecode
(see [chapter 11](docs/11-liquibase-locking.md)), captured by hand since there's no JVM assertion
that reads a compiled class's own constant pool.
## Findings worth the trip
- Flyway Community's `undo`, `diff`, `check`, `deploy`, `generate`, `model`, `prepare` and `auth` commands all **compile fine** and throw `FlywayRedgateEditionRequiredException` only at runtime — there is no working rollback in Flyway Community at all ([chapter 7](docs/07-why-there-is-no-undo.md)).
@@ -70,6 +70,6 @@ you can watch a real startup's bookkeeping instead of only reading about it.
## Going deeper
- [Flyway: how migrations work](https://documentation.red-gate.com/flyway/flyway-cli-and-api/concepts/migrations) — the vendor's own concept page (`rel="nofollow"`).
- [Flyway: how migrations work](https://documentation.red-gate.com/fd/migrations-271585107.html) — the vendor's own concept page (`rel="nofollow"`).
- [Liquibase: how it works](https://docs.liquibase.com/concepts/introduction-to-liquibase.html) — the vendor's own concept page (`rel="nofollow"`).
- [Spring Boot 4.1 release notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.1-Release-Notes) — for the package relocation both tools' autoconfiguration went through (chapter [2](02-anatomy-of-a-migration-run.md) touches this).
@@ -95,5 +95,5 @@ raw report query against a Flyway-managed schema.
## Going deeper
- [Flyway migration naming](https://documentation.red-gate.com/flyway/flyway-cli-and-api/concepts/migrations#naming) — the full naming grammar, including undo and repeatable prefixes (`rel="nofollow"`).
- [Flyway migration naming](https://documentation.red-gate.com/fd/migrations-271585107.html) — the full naming grammar, including undo and repeatable prefixes (`rel="nofollow"`).
- H2's identifier case sensitivity is documented on the [H2 SQL grammar page](https://h2database.com/html/grammar.html#name) under `DATABASE_TO_LOWER`/`DATABASE_TO_UPPER` (`rel="nofollow"`) — this module doesn't set either, so it runs on H2's default.
@@ -43,4 +43,4 @@ themselves, not a schema-drift detector.
## Going deeper
- [`spring.flyway.validate-on-migrate`](https://docs.spring.io/spring-boot/appendix/application-properties/index.html#application-properties.data-migration.spring.flyway.validate-on-migrate) in the Spring Boot configuration reference (`rel="nofollow"`) — `true` by default.
- [Flyway's `repair` command](https://documentation.red-gate.com/flyway/flyway-cli-and-api/commands/repair) (`rel="nofollow"`) — what actually happens to `flyway_schema_history` when you run it, and why it's a deliberate, logged action rather than something to script into a startup hook.
- [Flyway's `repair` command](https://documentation.red-gate.com/flyway/reference/commands/repair) (`rel="nofollow"`) — what actually happens to `flyway_schema_history` when you run it, and why it's a deliberate, logged action rather than something to script into a startup hook.
@@ -64,5 +64,5 @@ installed_rank | version | description | success
## Going deeper
- **`outOfOrder` is a blunt, global switch** — turning it on doesn't just allow the one late migration you're expecting, it allows *any* lower-numbered migration to slot in from then on. Flyway's own docs call this out as reducing reproducibility, and `docs/output/03-flyway-out-of-order.txt`'s third-startup log line says so verbatim: `outOfOrder mode is active. Migration of schema may not be reproducible.`
- [`ignoreMigrationPatterns`](https://documentation.red-gate.com/flyway/flyway-cli-and-api/configuration/parameters/ignore-migration-patterns) (`rel="nofollow"`) — the error message's other suggested fix, for permanently ignoring a specific migration instead of relaxing ordering globally.
- [`ignoreMigrationPatterns`](https://documentation.red-gate.com/fd/flyway-ignore-migration-patterns-setting-277579002.html) (`rel="nofollow"`) — the error message's other suggested fix, for permanently ignoring a specific migration instead of relaxing ordering globally.
- A team that hits this regularly is usually missing a CI check that fails a PR when its migration's version number is lower than what's already merged to main — cheaper than relying on `outOfOrder` at all.
@@ -58,5 +58,5 @@ filename prefix.
## Going deeper
- **Repeatable migrations run last, after every pending versioned one**, in the order they appear on the classpath (alphabetically, by default) — not interleaved by when they were last changed.
- [Flyway's repeatable migration docs](https://documentation.red-gate.com/flyway/flyway-cli-and-api/concepts/migrations#repeatable-migrations) (`rel="nofollow"`) cover ordering and the `installedOn`/checksum comparison in full.
- [Flyway's repeatable migration docs](https://documentation.red-gate.com/fd/repeatable-migrations-273973335.html) (`rel="nofollow"`) cover ordering and the `installedOn`/checksum comparison in full.
- A view is the textbook use case, but the same mechanism works for anything idempotent — a stored procedure body, or a `MERGE`/`upsert` of reference data that should reflect whatever the file currently says, not whatever it said the first time it ran.
@@ -52,5 +52,5 @@ assertion or a broken monitoring query — this module's own first draft did exa
## Going deeper
- **`baselineVersion` decides what "already accounted for" means**, and it matters which value you pick — chapter [14](14-running-both-at-once.md) shows the same mechanism used with `baseline-version=0` instead of `1`, for a database whose existing schema *doesn't* match any of your migration files at all.
- [Flyway `baseline` command reference](https://documentation.red-gate.com/flyway/flyway-cli-and-api/commands/baseline) (`rel="nofollow"`) — the one-time `flyway baseline` CLI/API call this test's `baselineOnMigrate=true` triggers automatically on first startup.
- [Flyway `baseline` command reference](https://documentation.red-gate.com/flyway/reference/commands/baseline) (`rel="nofollow"`) — the one-time `flyway baseline` CLI/API call this test's `baselineOnMigrate=true` triggers automatically on first startup.
- Baselining is a one-way door in the sense that matters: once version 1 is marked as baselined, Flyway will never again check whether the schema it describes actually matches `V1__init.sql`'s content — that specific file's checksum is simply never looked at again for this database.
@@ -53,5 +53,5 @@ external coordinator, no separate lock table: the schema history table *is* the
## Going deeper
- **A naive assertion here would compare wall-clock time against the sum of both individual durations**, expecting serialization to look like "one after the other, end to end". That's the wrong model for two threads submitted at the same instant with one blocking on the other's lock — the right check is that *both* individual durations are long, proving the loser genuinely waited rather than racing ahead. This module's test was rewritten once to fix exactly that reasoning error.
- [Flyway's locking strategy](https://documentation.red-gate.com/flyway/flyway-cli-and-api/concepts/migrations#concurrent-migration) (`rel="nofollow"`) documents the row-lock approach and which databases support it natively versus via a fallback.
- [Flyway's locking strategy](https://documentation.red-gate.com/fd/migrations-271585107.html) (`rel="nofollow"`) documents the row-lock approach and which databases support it natively versus via a fallback.
- Liquibase solves the same problem with a dedicated, separate lock table rather than a row lock on the history table itself — chapter [11](11-liquibase-locking.md) measures how differently that behaves under contention.
@@ -78,6 +78,6 @@ ID | EVENT
## Going deeper
- [Which change types Liquibase can auto-generate a rollback for](https://docs.liquibase.com/workflows/liquibase-community/how-to-apply-or-revert-changes.html) (`rel="nofollow"`) — `createTable`, `addColumn`, and a handful of other structural changes; almost anything involving data (`insert`, `update`, `delete`, most `sql:` changes) needs an explicit `rollback:` block.
- [Which change types Liquibase can auto-generate a rollback for](https://docs.liquibase.com/community/user-guide-5-0/what-automatic-rollbacks-does-liquibase-support) (`rel="nofollow"`) — `createTable`, `addColumn`, and a handful of other structural changes; almost anything involving data (`insert`, `update`, `delete`, most `sql:` changes) needs an explicit `rollback:` block.
- **A team relying on rollback in production is really relying on discipline**: every changeset that touches data needs its rollback written and tested *at the time the changeset is written*, not discovered missing during an actual incident — `rollback(1, ...)` failing is the worst possible moment to learn `insert` has no inverse.
- Liquibase also supports `rollbackCount`, `rollbackToDate` and rolling back by tag — this module exercises only the single-changeset `rollback(int, ...)` overload.
@@ -26,14 +26,18 @@ ID | AUTHOR | EXECTYPE
The changeset ran exactly once — same guarantee as Flyway. The *shape* of the wait is different,
though, and it's a real, verified difference: instance B's call took over ten seconds, for 800ms
of underlying work. That's not noise. Liquibase's `LockService` doesn't retry immediately when a
lock is held — it polls, and `liquibase.changeLogLockPollRate`'s default, confirmed by reading
`GlobalConfiguration`'s own bytecode, is **10 seconds**:
lock is held — it polls, and `liquibase.changeLogLockPollRate`'s default, confirmed by decompiling
`GlobalConfiguration`'s own bytecode with `javap`, is **10 seconds**:
```
liquibase.changeLogLockPollRate default 10 (seconds between checks while the lock is held)
liquibase.changeLogLockWaitTimeInMinutes default 5 (minutes before giving up entirely)
liquibase.changeLogLockPollRate -> default 10 (seconds between checks while the lock is held)
liquibase.changeLogLockWaitTimeInMinutes -> default 5 (minutes before giving up entirely)
```
(from [`docs/output/16-liquibase-lock-defaults-javap.txt`](output/16-liquibase-lock-defaults-javap.txt),
the trimmed `javap -p -c -constants` output showing both `long` constants — `5` and `10` — right next
to the field they initialize)
<svg viewBox="0 0 700 150" xmlns="http://www.w3.org/2000/svg" font-family="monospace" font-size="13">
<text x="20" y="20">instance A</text>
<rect x="90" y="8" width="70" height="24" fill="#dcfce7" stroke="#16a34a"/>
@@ -32,4 +32,4 @@ Community jar.
## Going deeper
- This test is a snapshot of Liquibase 5.0.3's Community classpath, not a guarantee about future releases — the license service's existence at all, brand new in 5.0, is itself evidence that Liquibase's commercial boundary is actively being redrawn (chapter [13](13-the-fsl-license-change.md) covers why).
- [Liquibase Pro feature comparison](https://www.liquibase.com/liquibase-pricing) (`rel="nofollow"`) — what Pro actually adds, as separate functionality rather than unlocked stubs.
- [Liquibase Pro feature comparison](https://www.liquibase.com/pricing) (`rel="nofollow"`) — what Pro actually adds, as separate functionality rather than unlocked stubs.
@@ -0,0 +1,25 @@
================================================================================
Liquibase: confirming the default lock-poll and lock-wait settings via javap
================================================================================
captured: 2026-09-15T07:07:00.000000000Z
$ unzip -o -q liquibase-core-5.0.3.jar 'liquibase/GlobalConfiguration*.class' -d extracted
$ javap -p -c -constants -classpath extracted liquibase.GlobalConfiguration
-- static initializer, defining CHANGELOGLOCK_WAIT_TIME --
69: ldc #88 // String changelogLockWaitTimeInMinutes
76: ldc #92 // String liquibase.changeLogLockWaitTimeInMinutes
81: ldc #94 // String Number of minutes to wait for the changelog lock to be available before giving up
86: ldc2_w #96 // long 5l
98: putstatic #101 // Field CHANGELOGLOCK_WAIT_TIME:Lliquibase/configuration/ConfigurationDefinition;
-- static initializer, defining CHANGELOGLOCK_POLL_RATE --
102: ldc #104 // String changelogLockPollRate
109: ldc #106 // String liquibase.changeLogLockPollRate
114: ldc #108 // String Number of seconds wait between checks to the changelog lock when it is locked
119: ldc2_w #110 // long 10l
131: putstatic #112 // Field CHANGELOGLOCK_POLL_RATE:Lliquibase/configuration/ConfigurationDefinition;
-- reading the two "long N" constants above off the bytecode directly --
liquibase.changeLogLockPollRate -> default 10 (seconds between checks while the lock is held)
liquibase.changeLogLockWaitTimeInMinutes -> default 5 (minutes before giving up entirely)