Find the Version of a Flyway-managed Database
Maintaining a version of a database opens a lot of possibilities, especially if an automated process can easily grab the current version, at runtime. You might, for example, have a routine that is only appropriate after a particular version. It is also very handy to be able to associate entries in an event log or bug report with the database version. The article describes various ways to get the current Flyway schema version from Flyway, and how to get it using SQL, in SQL Server, MySQL, PostgreSQL and SQLite.
first published:13 September 2022
If you are scripting a process in Flyway, you can get most of the information you need from environment variables. You get the connection details, credentials and your custom placeholders, for example. However, you can’t get the most essential fact, the current schema version of the database, from an environment variable. This is rather frustrating.
After all, if you are using callback script to back up the current version of database before migrating it, or to generate a build script after a migration run for the new version, you need to know what version to put in the filename of the backup file or build script. If you need to see what’s been changed by a migration, you will probably need to know not only that version but the previous one too. Alternatively, you might have a routine that it’s only appropriate to use after a particular version, or on a specific range of versions. It is also useful to be able to associate entries in an event log or bug report with the database version.
For all these reasons, it is very handy to know the schema version in a callback or migration script, and occasionally useful to be able to get that value in SQL too. So how do we get it?
Asking Flyway for the database schema version
When using Flyway, the built-in the definitive source of the current version is the current schema version deduced from the Flyway schema history table, which also records the history of the changes, who did them, how long they took, and the type of change.
Before we start, I’ll say that the best solution, in my view, would be for Flyway to make available an ephemeral schemaVersion
default placeholder, which we could use with the ‘after’ callbacks such as afterMigrate
, afterUndo
, afterInfo
, afterRepair
and afterVersioned
. You’d just add the placeholder to your SQL code and Flyway would perform the substitution before running the code. In a PowerShell or DOS script, you’d read the environment variable.
Unfortunately, they don’t provide this. However, until then, here’s your options to get the current schema version from Flyway.
Read the ASCII output of Flyway info
If you run flyway
info
, you’ll see the current schema version hidden in plain view just above the migration history table:
In fact, Flyway provides two returned values in its output, one of which is the schema version, and the other is the connection string. If all we need is the current version, we can get the latest schema version from Flyway and use that value to stamp the database with its version number, using a database comment or extended property. Yeah, good idea, especially if you can do this in a Flyway post-migration callback.
To get just the current version, you can just read the value with a regex match on the output (hoping that the format Flyway uses for its ASCII feedback doesn’t ever change): In this code, I’m working in the project directory and I’m reading a flyway CONF file with my credentials in it and I’ve saved the path to this file in $Credentials
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$Output = Get-content $Credentials -Raw | flyway info -configFiles=- $Match = ([regex]'Schema version: (?<Version>[\d\.]{1,30})').matches($Output) if ($Match.Count -gt 0) #we've got a revision back { $currentversion = ($Match.Groups | where { $_.Success -and $_.name -eq 'Version' }).Value } else { if ($output -like '*<< Empty Schema >>*') { $currentversion = '0.0' } else { Write-Warning "No match for version from Flyway Info" } } Write-Verbose "The current Flyway version is $CurrentVersion" |
It works, but if you’ve got a few migration files, it gets slow because it calculates the checksum of all the migration files before running the info
command
Also, if you try this trick in an AfterEach…
callback or any other callback that gets called within the transaction used for the migration, then the system locks up entirely due to mutual blocking until you kill one of the sessions, and thereby the connection.
This means that, for any process that is making reference to the current version, and that version is changing (imagine the time soaked up just validating your migration files over and over again).
Read the JSON output
Instead of trying to read the ASCII output, you can run flyway
info
-outputType=json
, to persuade it to write the information out, grab the JSON output, and read the schemaVersion
value from this JSON document:
1 2 3 4 5 |
$Info=Get-content $Credentials -Raw | flyway info -configFiles=- -outputType=json|convertFrom-json if ($Info.exception -eq $null) {$SchemaVersion=$Info.schemaVersion} else {write-warning "Info Failed, with message '$($Info.Exception)' "} |
As well as the schema version, you get some other useful information such as the list of schemas, the list of migrations and the name of the database
The problem here is that you still need to run Flyway Info every time you need an up-to-date version. There is now a solution is that the latest Flyway versions now keep a similar file to this, the report.json, up to date every time it performs a migration. The bad news is that it also writes a huge report.HTML
with nothing useful in it for our purposes
Use the Flyway JSON report
The Flyway CLI now supports a -reportEnabled parameter, which when set to true will mean Flyway produces a JSON report when the version changes. This is easy to read via a procedural script such as Bash or PowerShell, though inaccessible to a SQL script.
Flyway saves the report.json
in the current working directory, on every change, not just on running the info
command. The downside is the added clutter of a report.html. To get around that, I put a flyway.reportEnabled=false
in my user profile (It is now false by default) and add a -reportEnabled=true
parameter whenever I need to get hold of the version.
1 2 3 4 5 6 7 |
$Output=Get-content $Credentials -Raw | flyway info -configFiles=- -reportEnabled=true if (Test-Path -Path 'report.html' -PathType Leaf){del 'report.html'} #who needs that? if (Test-Path -Path 'report.json' -PathType Leaf) {$CurrentVersions=Get-content 'report.json' -Raw|ConvertFrom-json} else {write-error "The report file 'report.json' wasn't generated"} $DatabaseVersion = $CurrentVersions.individualResults[$CurrentVersions.count-1].SchemaVersion |
Assuming this value is updated by all the Flyway actions, we now know the version just by reading this JSON file. The only problem with this is that it tells you just the current version, not the previous one, and there are times, such as when detecting changes, when you need this information.
Use the Flyway Teamwork framework
To work around all the limitations, I decided to provide a generic PowerShell routine, $GetCurrentVersion
, which was usable across all supported databases (see Cross-RDBMS Version Checks in Flyway). It’s available as part of the Flyway Teamwork framework for PowerShell. It can calculate the version that was the result of an undo or repeatable migration, within PowerShell, because it can compare version numbers.
Getting the schema version in SQL
If none of the previous options suit, then you’ll need a reliable way to retrieve the schema version using SQL. You can get the current version in a SQL script by querying the flyway schema table directly.
Again, though, there are a few complications
Getting the name and location of the schema history table
The first complication that you have when using SQL is that the name and schema of this table isn’t fixed. To do this query, you’ll need to know the name and schema of the Flyway Schema History table. Unfortunately, this isn’t set in stone. When you first create a Flyway project, you can either accept the default name and location, or you can change either setting in the project-level configuration (if you want to change the table name or schema in an existing project then you need to be careful, because it isn’t entirely straightforward).
Since you are executing your SQL in Flyway (e.g. in a SQL callback or a script migration), you can, and should, use Flyway placeholders for the name location of the table, and these then get filled in automatically by Flyway before running the SQL script. You can now easily place, in your database, a routine such as a procedure that gets the version from whatever name and schema location you’ve specified for the table previously known as flyway_schema_history
.
These placeholders are provided by:
${flyway:defaultSchema}
– the default schema for Flyway${flyway:table}
– name of the Flyway schema history table
So, in a Flyway SQL Script, these placeholders will allow you to do a query that will give you the current version of the database
Migrations aren’t always upwards
If you only ever use versioned (V) migration scripts, and never Undo or Repeatable migrations, and you only want the current version, then getting the value from SQL couldn’t be simpler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
--SQL Server SELECT TOP 1 version FROM ${flyway:defaultSchema}.${flyway:table} ORDER BY installed_rank DESC; --PostgreSQL SELECT version FROM ${flyway:defaultSchema}.${flyway:table} ORDER BY installed_rank DESC LIMIT 1; --Oracle SELECT version FROM ${flyway:defaultSchema}.${flyway:table} ORDER BY installed_rank DESC FETCH FIRST 1 ROWS ONLY; --MySQL and MariaDB SELECT version FROM ${flyway:defaultSchema}.${flyway:table} ORDER BY installed_rank DESC LIMIT 1; -- SQLite SELECT version FROM ${flyway:defaultSchema}.${flyway:table} ORDER BY installed_rank DESC LIMIT 1; |
However, this isn’t accurate under all circumstances. In many Flyway projects, migrations aren’t always upwards. Repeatable migrations have no version and leave the current version as NULL. In Flyway Teams projects, you can also use undo (U) migrations to reverse to a previous version. These U migrations have the same version number as their V file counterpart, so the schema version returned by the previous queries won’t be the version that was the result of the undo, but the version that was undone.
The SQL logic to obtain the current and previous version in those circumstances isn’t straightforward. You might assume that you need to compare or order version numbers, which is not a datatype that is supported in SQL. Since there is a reliable incrementing key, the installed_rank
, it is possible to avoid that, but the results aren’t entirely pretty.
If you are using SQL, then you can get a direct substitution of the actual version if you can provide this as a placeholder. Before Flyway executes a SQL migration, it sees whether it can swap out any of the placeholders, either the built-in ones or the custom ones, with their current values. You would have to maintain this value with a callback routine that checked the version after a migration.
Reliably reporting the schema version using SQL
To deal with the problem of undo migrations, where the result is to revert to the previous version, we need to be rather cleverer. The easiest approach is to use a procedure or, preferably a function because MySQL/MariaDB doesn’t support batches with variables and conditional blocks/control structures. It also allows us to compile a routine that records the location of the schema history table, so we can use it independently from Flyway. We can’t use this approach with SQLite but can provide something that works when executed by Flyway, and this will work with all major relational databases.
While we’re about it, we’ll collect the previous version as well as the current one. There is no way of knowing this version in an automated process otherwise because versions just need to be ordered, but without any fixed increment. It is also handy to know how we got to this version, so we ought to return that. We’ll aim to provide the following:
- ‘Current Version’ – this must always match the value that
Flyway info
reports forschemaVersion
. - ‘Current Type’ – the type of migration that produced the current version (SQL, Java etc.)
- ‘Previous Version’ – the version that the preceding migration in the chain produced.
- ‘Previous Type’ – the type of migration that got to the previous version
- ‘Last Action Type’ – the type of action that produced the current version
This extra information can be useful. I use it to help to determine what was changed by the last migration run. Here is an example. I took the Pubs database up to version 1.1.12 (in SQL Server, in this example). The function reported this:
Then, I did an undo run all the way back to version 1.1.7. This time, the function reported this…
This tells us that we slid right back down from version 1.1.12 to 1.1.7 via an UNDO_SQL migration run.
Basically, you need to read the Flyway schema table to work out which was the final migration. If, upon investigation, it wasn’t an undo or repeatable migration, it is easy. You just read the version number and calculate the previous one. Otherwise, it is not easy.
- Repeatable – if the last migration was a repeatable, the current version will be
NULL
because there was no version number specified in the entry for the repeatable file. In that case, we look back until we find the row with the last versioned migration and hold onto that. - Undo – if the last migration was an undo, the version number it provides is the version that Flyway undid, not the current version. To find out what the previous version was, we must find what version was the result of the last successful forward migration. This isn’t easy because an undo chain could have happened.
All the following code examples, for each of the RDBMSs, use the raw table name, rather than the Flyway version. Every mention of dbo.flyway_schema_history
must be replaced with ${flyway:defaultSchema}.${flyway:table}
if you are going to run it from Flyway. I’ve left these with the default name and schema just so you can try it out in your favorite IDE.
SQL Server
In SQL Server, we will need to force the version number into a hierarchy datatype. This will allow us to compare and sort by version number. Database projects often have a separate utilities schema, but in this example, I’ve put the function in the dbo
schema of the database, as there is no common standard for its location.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
IF (Object_Id ('dbo.FlywayVersion') IS NULL) EXEC ('CREATE FUNCTION dbo.FlywayVersion() RETURNS @returntable TABLE (dummy char(1)) AS BEGIN RETURN END'); GO /* what we used to do before CREATE OR ALTER */ /** Summary: > This returns a one-row table that tells us what the current flyway version is, and what the previous one was. Author: Philip Hypotenuse Factor Date: Thursday, 8 September 2022 Examples: - Select * from dbo.FlywayVersion() - SELECT Current_Version, Current_Type, Previous_Version, Previous_Type, Last_Action_Type FROM dbo.FlywayVersion() Returns: > nothing **/ ALTER FUNCTION dbo.FlywayVersion () RETURNS @WhatHappened TABLE (Current_Version NVARCHAR(50) NOT NULL, Current_Type NVARCHAR(20) NOT NULL, Previous_Version NVARCHAR(50) NULL, Previous_Type NVARCHAR(20) NULL, Last_Action_Type NVARCHAR(20) NOT NULL) AS /*define our variables */ BEGIN DECLARE @Latest_Installed_Rank INT, @CurrentVersion NVARCHAR(50), @PreviousVersion NVARCHAR(50), @CurrentType NVARCHAR(20), @PreviousType NVARCHAR(20), @LastActionType NVARCHAR(20); /* we find out the last action taken, the version number and type*/ SELECT @Latest_Installed_Rank = installed_rank, @CurrentVersion = version, @CurrentType = type, @LastActionType = type FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE version IS NOT NULL); -- check for result of REPEATABLE /* Now we assume that it was not an undo so we find out the version number achieved by the last forward migration */ SELECT @PreviousVersion = version, @PreviousType = type FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE installed_rank < @Latest_Installed_Rank AND type NOT LIKE 'UNDO%'); /*oh. Complication it was an undo */ IF (@CurrentType LIKE 'UNDO%') BEGIN /* in this case the previous version was that of the last forward migration */ SELECT @PreviousVersion = version FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE type NOT LIKE 'UNDO%'); /* in this case the current version is the one before the one in the entry that turned out to be an UNDO*/ SELECT TOP 1 @CurrentVersion = version, @CurrentType = type FROM dbo.flyway_schema_history WHERE Cast ('/' + version + '/' AS HIERARCHYID) < Cast ('/' + @CurrentVersion + '/' AS HIERARCHYID) ORDER BY installed_rank DESC; END; INSERT INTO @WhatHappened (Current_Version, Current_Type, Previous_Version, Previous_Type, Last_Action_Type) VALUES (@CurrentVersion, -- Current_Version - nvarchar(50) @CurrentType, -- Current_Type - nvarchar(20) @PreviousVersion, -- Previous_Version - nvarchar(50) @PreviousType, -- Previous_Type - nvarchar(20) @LastActionType -- Last_Action_Type - nvarchar(20) ); RETURN; END; |
So, there we have it. What about MySQL/MariaDB?
MySQL/MariaDB
We’ll leave out the comment block as it is rather repetitive. Here, the only datatype that seems to be useful for sorting versions is the internet address datatype (INET_ATON
). There is no table-valued function in MySQL, so we use a procedure instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
DELIMITER // create or replace procedure FlywayVersion() BEGIN DECLARE vLatest_Installed_Rank INT; DECLARE vCurrentVersion VARCHAR(50); DECLARE vPreviousVersion VARCHAR(50); DECLARE vCurrentType VARCHAR(20); DECLARE vPreviousType VARCHAR(20); DECLARE vLastActionType VARCHAR(20); /* we find out the last action taken, the version number and type */ SELECT installed_rank, `version`,`type`,`type` INTO @vLatest_Installed_Rank, @vCurrentVersion, @vCurrentType, @vLastActionType FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT MAX(installed_rank) FROM dbo.flyway_schema_history WHERE `version` IS NOT NULL); -- check for result of REPEATABLE ); /* Now we assume that it was not an undo so we find out the version number achieved by the last forward migration */ SELECT `version`, `type` INTO @vPreviousVersion, @vPreviousType FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT MAX(installed_rank) FROM dbo.flyway_schema_history WHERE installed_rank < @vLatest_Installed_Rank AND `type` NOT LIKE 'UNDO%'); /*oh. Complication it was an undo */ IF @vCurrentType LIKE 'UNDO%' THEN /* in this case the previous version was that of the last forward migration */ SELECT `version` INTO @vPreviousVersion FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE `type` NOT LIKE 'UNDO%'); /* in this case the current version is the one before the one in the entry that turned out to be an UNDO*/ SELECT `version`, `type` INTO @vCurrentVersion, @vCurrentType FROM dbo.flyway_schema_history WHERE INET_ATON(substring_index(CONCAT(`version`,'.0.0.0'),'.',4)) < INET_ATON(substring_index(CONCAT( @vCurrentVersion,'.0.0.0'),'.',4)) ORDER BY installed_rank DESC LIMIT 1; END if; SELECT @vCurrentVersion AS `CurrentVersion`, @vCurrentType AS `CurrentType`, @vPreviousVersion AS `PreviousVersion`, @vPreviousType AS `PreviousType`, @vLastActionType AS `LastActionType`; END; // DELIMITER ; CALL flywayVersion(); |
PostgreSQL
The PostgreSQL version converts the version into an array and does a comparison on those lines. In principle, it is like the MySQL/MariaDB version except that it works as a table-valued function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
create or replace function dbo.FlywayVersion() returns table ( Current_Version VARCHAR(50), Current_Type VARCHAR(20), Previous_Version VARCHAR(50), Previous_Type VARCHAR(20), Last_Action_Type VARCHAR(20) ) language plpgsql AS ' declare vLatest_Installed_Rank INT; vCurrentVersion VARCHAR(50); vCurrentType VARCHAR(20); vPreviousVersion VARCHAR(50); vPreviousType VARCHAR(20); vLastActionType VARCHAR(20); begin SELECT installed_rank, version,type,type INTO vLatest_Installed_Rank, vCurrentVersion, vCurrentType, vLastActionType FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT MAX(installed_rank) FROM dbo.flyway_schema_history WHERE version IS NOT NULL); -- check for result of REPEATABLE ); /* Now we assume that it was not an undo so we find out the version number achieved by the last forward migration */ SELECT version, type INTO vPreviousVersion, vPreviousType FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE installed_rank < vLatest_Installed_Rank AND type NOT LIKE ''UNDO%''); IF vCurrentType LIKE ''UNDO%'' THEN /* in this case the previous version was that of the last forward migration */ SELECT version INTO vPreviousVersion FROM dbo.flyway_schema_history WHERE installed_rank = (SELECT Max (installed_rank) FROM dbo.flyway_schema_history WHERE type NOT LIKE ''UNDO%''); /* in this case the current version is the one before the one in the entry that turned out to be an UNDO */ SELECT version, type INTO vCurrentVersion, vCurrentType FROM dbo.flyway_schema_history WHERE string_to_array(version, ''.'')::INT[] < string_to_array(vCurrentVersion, ''.'')::INT[] ORDER BY installed_rank DESC LIMIT 1; END if; return query -- SELECT version FROM dbo.flyway_schema_history where version is not null ORDER BY string_to_array(version, ''.'')::INT[] DESC LIMIT 1; Select vCurrentVersion ,vCurrentType , vPreviousVersion, vPreviousType, vLastActionType; end;'; |
We can call this by:
1 |
SELECT * FROM dbo.FlywayVersion(); |
SQLite
You’d have thought that, because SQLite has no way of comparing or ordering version numbers, has no IF…THEN…ELSE
logic, no variables or procedures, then it would be impossible. OK, let’s make a small compromise and return just the version number. If we allow ourselves to make several queries into the history table, then it is reasonably easy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
SELECT CASE WHEN ( -- check whether we are dealing with an UNDO Select type FROM flyway_schema_history WHERE installed_rank = (SELECT Max(installed_rank) FROM flyway_schema_history WHERE success = 1) ) LIKE 'UNDO%' /* IF so then we need to get the next version before the UNDO version */ THEN -- if it is an UNDO (select Version FROM flyway_schema_history where version is not NULL -- probably a pesky repeatable and success = true AND TYPE NOT LIKE 'UNDO%' AND installed_Rank<( /*the latest migration that is lower then the version quoted in the UNDO operation which is actually the version you are undoing */ select min(installed_rank) FROM flyway_schema_history where version = ( Select version FROM flyway_schema_history WHERE installed_rank = (SELECT Max(installed_rank) FROM flyway_schema_history WHERE success = 1 ) ) and success = 1 ) ORDER BY installed_rank desc LIMIT 1 ) ELSE -- then it is simple (Select version FROM flyway_schema_history WHERE installed_rank = (SELECT Max(installed_rank) FROM flyway_schema_history WHERE success = 1 ) ) END AS TheVersion |
You might look at this and think ‘why don’t we just do this for all the RDBMSs?’ The hitch is that it must be executed by Flyway if you aren’t using the standard name of the table. Also, it is quite long and slightly opaque, and if you were to provide all the information of the other versions, it would be even longer, and it is already hitting the flyway_schema_history
table up to six times. Yes, it is a worthwhile compromise, but it is still a compromise.
Roundup
If you are using a script, the easiest way to get the current version is to use flyway
info
, but this isn’t always possible when you are using a callback script, and it can be painfully slow. Flyway now keeps this information current in the report.json file for the project, so it can easily be read from there.
If you just want to find the current version of a flyway database from SQL within a Flyway SQL script, or you don’t want a function, then the code for SQLite will work with minor modifications for all the RDBMS I’ve covered (SQL Server uses TOP
rather than LIMIT
. PostgreSQL insists on success being true
rather than 1
, because Flyway defines the Success
column for PostgreSQL as being Boolean
rather than bit
).
You’ll also need to replace flyway_schema_history
with ${flyway:defaultSchema}.${flyway:table}
. Remember, though, that the current name of the flyway table is available to a script or callback run by Flyway, it is not guaranteed to work outside Flyway, unless you can find a way of knowing for certain the name and schema of the flyway history table. This is easily obtained by any callback script that just writes the information to a file. This information, along with much else, is presented to you by my Flyway Teamwork framework.
Hopefully, I’ve given you a few solutions that should work for you to find out the version number of your flyway database via SQL, Dos Batch, Bash or PowerShell.