<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>DiffyPick Blog — flyway</title><description>Posts tagged &quot;flyway&quot; from the DiffyPick Blog.</description><link>https://diffy-pick.com/</link><language>en</language><item><title>What Flyway validate actually checks</title><link>https://diffy-pick.com/blog/what-flyway-validate-actually-checks/</link><guid isPermaLink="true">https://diffy-pick.com/blog/what-flyway-validate-actually-checks/</guid><description>Flyway validate compares file checksums, not your database. What Flyway and Liquibase each verify, what drift detection costs, and what falls through.</description><pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Tool capabilities and edition boundaries described here are accurate as of publication. Both vendors move these lines; check current documentation before making decisions based on this post.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There’s a specific moment this post is written for: your pipeline runs &lt;code&gt;flyway validate&lt;/code&gt;, everything is green, and you conclude that your database matches your migrations.&lt;/p&gt;
&lt;p&gt;That conclusion doesn’t follow. Not because Flyway is broken, but because &lt;code&gt;validate&lt;/code&gt; answers a different question than the one you’re asking.&lt;/p&gt;
&lt;h2 id=&quot;what-validate-does&quot;&gt;What validate does&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-validate-does&quot; aria-label=&quot;Link to this section&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When Flyway applies a migration, it computes a checksum of the file (CRC32 for SQL migrations) and stores it in the &lt;code&gt;flyway_schema_history&lt;/code&gt; table alongside the version, description, and execution details.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; recomputes checksums for the local migration files and compares them against what’s stored in the history table. It also checks that applied migrations still exist locally and flags pending or missing ones.&lt;/p&gt;
&lt;p&gt;In other words, &lt;code&gt;validate&lt;/code&gt; answers: &lt;strong&gt;“Have the migration files changed since they were applied, and is the file set consistent with the recorded history?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It’s a file-integrity check. A good one — it catches the classic incident where someone edits an already-applied migration (even just fixing a typo in a comment) and every environment that applied the original version now disagrees with the repository.&lt;/p&gt;
&lt;h2 id=&quot;what-it-never-looks-at&quot;&gt;What it never looks at&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-it-never-looks-at&quot; aria-label=&quot;Link to this section&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Notice what’s absent from that description: &lt;strong&gt;the schema itself.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;validate&lt;/code&gt; reads exactly one thing from your database — the history table. It never inspects your actual tables, columns, indexes, or constraints. So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run a manual &lt;code&gt;ALTER TABLE&lt;/code&gt; in production → files unchanged, history unchanged → &lt;strong&gt;validate passes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Another tool adds an index → &lt;strong&gt;validate passes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Someone drops a constraint during an incident and forgets → &lt;strong&gt;validate passes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is a bug. The tool is doing precisely what it’s scoped to do. The problem is the widespread belief that it’s scoped to do more.&lt;/p&gt;
&lt;p&gt;A useful way to hold it in your head:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Does &lt;code&gt;validate&lt;/code&gt; answer it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Were my migration files edited after being applied?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are there migrations recorded as applied that no longer exist locally?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are there pending migrations not yet applied?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does my database schema match what the migrations describe?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did anyone change the schema outside of Flyway?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last two rows are schema drift, and they are outside the jurisdiction of every command in the default Flyway workflow.&lt;/p&gt;
&lt;p&gt;A related footnote: &lt;code&gt;repair&lt;/code&gt; doesn’t fix your schema either. It updates the history table — realigning stored checksums with current files, removing failed migration entries. It’s history bookkeeping, same jurisdiction as &lt;code&gt;validate&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-flyway-actually-offers-for-drift&quot;&gt;What Flyway actually offers for drift&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-flyway-actually-offers-for-drift&quot; aria-label=&quot;Link to this section&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Flyway does address real drift detection — outside the free core workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;check -drift&lt;/code&gt;&lt;/strong&gt; compares a target database against the state your migrations should have produced, using snapshots, and generates a report. Recent versions can store a snapshot inside the target database on each successful deploy (&lt;code&gt;-saveSnapshot&lt;/code&gt;), so the next run can detect anything that changed out-of-band since. It can also gate deployments: drift found, deployment aborted. This lives in the paid tier — at the time of writing, Flyway’s editions have consolidated such that drift detection is an Enterprise capability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Community Drift Check&lt;/strong&gt; exists for PostgreSQL specifically: it compares schema state after your last migration against the state found before the next run, and surfaces out-of-process changes. The nuance: results are viewed through Flyway Pipelines, Redgate’s hosted service — so “free” here means free-with-an-account, wired into their platform.&lt;/p&gt;
&lt;p&gt;Both are real capabilities. Neither is the thing most teams have running.&lt;/p&gt;
&lt;h2 id=&quot;the-liquibase-side&quot;&gt;The Liquibase side&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-liquibase-side&quot; aria-label=&quot;Link to this section&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Liquibase draws its lines differently, and more generously at the free tier.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;diff&lt;/code&gt;&lt;/strong&gt; compares two databases (or a database against a snapshot) and reports differences. &lt;strong&gt;&lt;code&gt;diff-changelog&lt;/code&gt;&lt;/strong&gt; goes further and emits changesets that would resolve them. Both are in the open-source distribution.&lt;/p&gt;
&lt;p&gt;Two caveats from the practical side:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Liquibase’s own documentation is direct about &lt;code&gt;diff-changelog&lt;/code&gt; output needing human inspection before deployment — some objects and dependencies can’t be represented automatically. That matches everything I’ve written elsewhere about diff generation being only part of the job.&lt;/li&gt;
&lt;li&gt;The operational conveniences around drift — like severity flags that turn “unexpected object found” into a configurable CI exit code — are Pro features. OSS gives you the comparison; making it enforce policy in a pipeline is where the paid tier starts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also worth knowing if you’re evaluating today: Liquibase has split OSS and Pro into distinct distributions (the 5.0 restructuring), with the OSS core slimmed down and extensions moved to on-demand installation. The capability boundaries above are exactly the kind of thing that shifts during such transitions — hence the note at the top of this post.&lt;/p&gt;
&lt;h2 id=&quot;the-gap-that-remains&quot;&gt;The gap that remains&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-gap-that-remains&quot; aria-label=&quot;Link to this section&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Put the pieces together and the landscape looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The command everyone runs (&lt;code&gt;validate&lt;/code&gt;) checks files, not schemas.&lt;/li&gt;
&lt;li&gt;The commands that check schemas exist, but sit behind paid tiers, hosted platforms, or changelog-centric workflows with real setup cost.&lt;/li&gt;
&lt;li&gt;The result, in most teams: &lt;strong&gt;drift detection is technically available and practically absent.&lt;/strong&gt; Nobody runs it nightly. Nobody runs it before deployments. It gets run for the first time during the incident retrospective.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want the checking habit without the licensing conversation, the previous post in this series builds a &lt;a href=&quot;https://diffy-pick.com/blog/how-to-compare-two-postgresql-schemas/&quot;&gt;pg_dump-based drift check&lt;/a&gt; you can put in CI today. And the &lt;a href=&quot;https://diffy-pick.com/blog/why-schema-drift-goes-undetected/&quot;&gt;first post&lt;/a&gt; covers why this gap exists at the level of the workflow itself, not any particular tool.&lt;/p&gt;
&lt;p&gt;The one-sentence takeaway: &lt;strong&gt;green &lt;code&gt;validate&lt;/code&gt; means your files are consistent with your history — it is not evidence about your database.&lt;/strong&gt; Treat it as such, and the mystery production column stops being a mystery.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I build &lt;a href=&quot;https://diffy-pick.com/&quot;&gt;DiffyPick&lt;/a&gt;, a desktop tool for comparing live database schemas directly — the “look at the actual database” half of this problem.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>database-migration</category><category>flyway</category><category>liquibase</category></item></channel></rss>