Testing a Flyway Database Migration
Often, we want to test the new version of a database, produced by a Flyway migration, before committing the new migration file, or to test the same migration run on a number of different databases. This article demonstrates how to do it, by generating and using JSON parameter files to run a series of Flyway actions on any number of databases, on any number of servers.
There are several reasons why you might want to maintain the same version of a database in more than one development database, probably in different server environments. You might need to run a preliminary test to make sure that a new version, created by a Flyway migration, works as expected, before committing the new migration file to your branch. You may need to test the same database running on different versions of the database system. You may need to test several variants of the database, running a series of tests in a post-migration script.
For these and other reasons you need to be prepared to do a migration run more than once. It must be easy to specify any number of databases, on any number of servers, for a Flyway migration, and so migrate them all, in turn, to the same version. We can do this most easily by executing Flyway from PowerShell. In this example we’ll generate JSON parameter files, storing all the required details for each database that we wish to migrate. We read all these parameter files, connecting to each database in turn, and on each one run the same series of Flyway actions, such as a clean followed by a migrate. If any errors or warnings occur, at any stage, we capture the output in a JSON doc, and report the problems. Any errors will abort the process unless we opt instead to try the next version of the database. Otherwise, we get a report verifying that each of the database we specified is now at the new version.
In this example, I’m using a SQL Server database, but it will do any Flyway database if you modify the way that the URL variable is created.
Creating the JSON parameter files
This routine stores all the details for the databases in a parameter directory. Unless you need to make changes, you need only do this once.
First, it defines the common parameters that will apply to the whole project. Then it contains an array of objects, each one of which represents a database. Each database could be on a different server, or they could all be on the same server, with different database names. If the databases are on different servers, there may be different User IDs executing the migrations.
Each object is used to create an array of Flyway parameters and a few variables that are needed to process the Flyway Actions (e.g., Migrate). You can, for example, specify the order in which they are executed. You can also specify whether the process should be aborted if an error occurs with the Flyway commands you execute. At different times, you may want to react in differently, if there was an error, and you can specify this accordingly. Each of the resulting objects is then saved as a JSON file. No passwords are saved in these files, just usernames.
Once we’ve saved all this information in a JSON document, one for each copy of the database we are migrating, we then store them in a directory. If this directory is on the network, then a team can then use this information. As these files contain JSON documents, it is easy to change them in situ.
The following sample is set up to run a ‘test’ migration on a test server, maybe a local laptop or a handy test server somewhere in the network. It only proceeds to the main migration if the test went well. When you run this for real, you’ll want to specify the database-specific integration tests that need to pass before the migration is applied. These can most easily be applied as a post-migration SQL Callback.
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 |
<# specify where to store the parameter files that are subsequently read by the routine that does that flyway operations to all the databases that have a parameter file #> $WhereToStoreIt = "$env:TMP\Flyway\Installations" # create the directory if it doesn't yet exist if (-not (Test-Path "$WhereToStoreIt" -PathType Container)) { $null = New-Item -ItemType directory -Path "$WhereToStoreIt" -Force } <# clean out the existing files #> Get-ChildItem "$WhereToStoreIt\*.JSON" | Remove-Item <# All data in common goes here. These apply to the project, not the specific database #> $common = @{ rdbms = 'sqlserver'; ProjectName = 'Publications'; ProjectFolder = '<PathToScripts>\PubsAndFlyway\PubsFlywayLittleMigration'; ProjectDescription = 'A sample project to demonstrate Flyway, using the old Pubs database'; Schemas = 'dbo,Classic,people'; username = '<User_ID>'; } <# now a list of all the databases you want to migrate to the version in the project folder. We assume that the migration scripts are in a 'scripts' subdirectory of the project folder #> @( @{ NameOfInstallation = 'Test'; order = '1'; Server = '<MyOtherServer>'; Database = '<MySecondDatabase>'; port = '1433'; OnError = 'Abort' AbortMessage = 'Something went wrong running <name>''s migration' }, @{ NameOfInstallation = 'Dev'; order = '2'; Server = '<MyServer>'; Database = '<MyFirstDatabase>'; port = '1433'; OnError = 'Continue' AbortMessage = 'Database migration of <name> failed' } <#Now we create a parameter file for each database. This contains the flyway parameters done as Flyway likes 'em. #> ) | foreach{ $_ += $common; # add a bit of error-checking. Is the project directory there if (-not (Test-Path "$($_.ProjectFolder)")) { Write-Error "Sorry, but I couldn't find a project directory at the $($_.ProjectFolder)location" } # ...and is the script directory there? if (-not (Test-Path "$($_.ProjectFolder)\Scripts")) { Write-Error "Sorry, but I couldn't find a scripts directory at the $($_.ProjectFolder)\Scripts location" } # create the flyway parameters if ([string]::IsNullOrEmpty($_.username)) #then it is using SQL Server Credentials { <# this is specific to SQL Server on Windows. You'll need to alter this for other databases #> $FlywayArgs = @("-url=jdbc:$($_.rdbms)://$($_.Server):$($_.port);databaseName=$($_.Database);integratedSecurity=true"); } else { $FlywayArgs = @("-url=jdbc:$($_.rdbms)://$($_.Server):$($_.port);databaseName=$($_.Database)", "-user=$($_.UserName)"); } #Now add in every other parameter $FlywayArgs += <# the project variables that we reference with placeholders #> @( "-locations=filesystem:$($_.ProjectFolder)\Scripts", <# the migration folder #> "-schemas=dbo,Classic,people", "-placeholders.projectDescription=$ProjectDescription", "-placeholders.projectName=$ProjectName"); <# here we add the variables we need in the routine as well as the Flyway Args#> @{ 'FlywayArgs' = $FlywayArgs; 'username' = $_.Username; 'Server' = $_.Server; 'rdbms' = $_.Rdbms; 'OnError' = $_.OnError; 'AbortMessage' = $_.AbortMessage; 'ProjectFolder' = $_.ProjectFolder; 'NameOfInstallation' = $_.NameOfInstallation; } | ConvertTo-json > "$WhereToStoreIt\$($_.order)$($_.NameOfInstallation).JSON" } |
Using the parameter files in a Flyway migration
When we want to do a set of migrations, we execute a second PowerShell routine. In the directory of parameter files, I’ve set up just two files, Test and Dev. This second routine just processes each file in order, picks up passwords, if required, from an encrypted file in your user directory, and performs the Flyway operations using the data. It captures the output from Flyway, as a JSON document, and responds to any errors. You’d probably also want to use the output in a report. I show how to do this in Using Flyway Output in PowerShell.
Here is a typical error from a run, where Flyway could not run the initial clean command due to a login failure:
WARNING: at the CLEAN command of Test DB_CONNECTION Unable to obtain connection from database (jdbc:sqlserver://<MyOtherServer>:1433;databaseName= <MySecondDatabase>) for user '<User_ID>': Cannot open database "<MySecondDatabase>" requested by the login. The login failed. ClientConnectionId:5dd63054-2b41-49fe-9c86-38e8f6f6c997 ------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------- -------------------------------- SQL State : S0001 Error Code : 4060 Message : Cannot open database "<MySecondDatabase>" requested by the login. The login failed. ClientConnectionId:5d d63054-2b41-49fe-9c86-38e8f6f6c997
Because I’ve specified that an error should abort the whole process it hasn’t gone on to attempt the migration. So, let’s get everything right and see what happens. OK, I’ll admit that I’m just running a tiny database migration, because I want to get it on the page!
category version description type executionTime -------- ------- ----------- ---- ------------- Versioned 1.1.1 Original Altered For SQL Server 2017 SQL 1395 Versioned 1.1.2 Added Tags Etc SQL 663 Sum Max Minimum Ave --- --- ------- --- 00 mins, 02 secs. 00 mins, 01 secs. 00 mins, 01 secs. 00 mins, 01 secs. Name Value ---- ----- database <MySecondDatabase> flywayVersion 8.0.0 initialSchemaVersion migrationsExecuted 2 operation migrate schemaName dbo, Classic, people targetSchemaVersion 1.1.2 category version description type executionTime -------- ------- ----------- ---- ------------- Versioned 1.1.1 Original Altered For SQL Server 2017 SQL 718 Versioned 1.1.2 Added Tags Etc SQL 547 Sum Max Minimum Ave --- --- ------- --- 00 mins, 01 secs. 00 mins, 01 secs. 00 mins, 01 secs. 00 mins, 01 secs. Name Value ---- ----- database <MyFirstDatabase> flywayVersion 8.0.0 initialSchemaVersion migrationsExecuted 2 operation migrate schemaName dbo, Classic, people targetSchemaVersion 1.1.2
So, here is the script, in this case running a clean followed by a migrate, but you can alter what happens before the migrate, as you require. Most obviously, you would probably just run the new migration file that you’ve just created without running the clean. The code would be much less daunting without the extra work to produce a nice report, but I wanted to show the benefits or running Flyway from PowerShell.
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 83 84 85 86 87 88 89 90 91 92 93 94 |
#Set-Alias Flyway 'C:\ProgramData\chocolatey\lib\flyway.commandline\tools\flyway-8.0.2\flyway.cmd' -Scope local $FlywayCommand = (Get-Command "Flyway" -ErrorAction SilentlyContinue) if ($null -eq $FlywayCommand) { throw "This script requires Flyway to be installed and available on a PATH or via an alias" } if ($FlywayCommand.CommandType -eq 'Application' -and $FlywayCommand.Version -lt [version]'8.0.1.0') { throw "Your Flyway Version is too outdated to work" } $ThisWentWell = $True; #Think positively! <# specify where the parameter files are stored. These are read by the following script that does whatever flyway operations you specify to all the databases that have a parameter file. #> $WhereToStoreIt = "$env:TMP\Flyway\Installations" # for every file we find in the directory Get-Childitem "$WhereToStoreIt\*.JSON" | sort -Property 'Name' | foreach{ $params = [IO.File]::ReadAllText($_.FullName) | ConvertFrom-Json # now we get the password if necessary if (!([string]::IsNullOrEmpty($params.username))) #then it is using SQL Server Credentials { # we see if we've got these stored already $SqlEncryptedPasswordFile = "$env:USERPROFILE\$($params.username)-$($params.Server)-$($params.rdbms).xml" # test to see if we know about the password in a secure string stored in the user area if (Test-Path -path $SqlEncryptedPasswordFile -PathType leaf) { #has already got this set for this login so fetch it $SqlCredentials = Import-CliXml $SqlEncryptedPasswordFile } else #then we have to ask the user for it (once only) { # hasn't got this set for this login $SqlCredentials = get-credential -Credential $params.UserName # Save in the user area $SqlCredentials | Export-CliXml -Path $SqlEncryptedPasswordFile <# Export-Clixml only exports encrypted credentials on Windows. otherwise it just offers some obfuscation but does not provide encryption. #> } <# Add the password into the flyway parameters#> $params.FlywayArgs += @("-password=$($SqlCredentials.GetNetworkCredential().password)") } $FlywayArgs = $params.FlywayArgs $FlywayArgs += '-outputType=json' # so we can get the error from Flyway cd "$($params.ProjectFolder)\scripts" #go to the scripts directory <#here we do a CLEAN before we do the migration. You may want to do something else. That's cool. Just edit the script #> # Here we do the FLYWAY CLEAN command $ResultOfClean = Flyway clean @FlywayArgs | convertFrom-json # Now we check for errors if ($ResultOfClean.Error -ne $null) { #Uh oh, an error Write-warning "$($params.AbortMessage -replace '<name>', $params.NameOfInstallation) at the CLEAN command of $($params.NameOfInstallation) $($ResultOfClean.Error.errorCode) $($ResultOfClean.Error.message)" $ThisWentWell = $false; } else #we do the FLYWAY MIGRATE command only if the CLEAN worked { $ResultOfmigrate = Flyway migrate @FlywayArgs | convertFrom-json if ($ResultOfmigrate.Error -ne $null) { Write-warning "$($params.AbortMessage -replace '<name>', $params.NameOfInstallation) at the MIGRATE command of $($params.NameOfInstallation) $($ResultOfmigrate.Error.errorCode) $($ResultOfmigrate.Error.message)" $ThisWentWell = $false; } else #lets do a nice-looking report { if ($ResultOfmigrate.migrations.Count -gt 0) { #Lets report it all. $ResultOfmigrate.migrations | select category, version, description, type, ExecutionTime | format-table $TimespanFormat = "{0:mm} mins, {0:ss} secs." $ResultOfmigrate.migrations | Measure-Object -Property executionTime -Sum -Max -Min -Average | Select @{ Name = 'Sum'; Expression = { $TimespanFormat -f (New-TimeSpan -Seconds ($_.Sum/1000)) } }, @{ Name = 'Max'; Expression = { $TimespanFormat -f (New-TimeSpan -Seconds ($_.Maximum/1000)) } }, @{ Name = 'Minimum'; Expression = { $TimespanFormat -f (New-TimeSpan -Seconds ($_.Minimum/1000)) } }, @{ Name = 'Ave'; Expression = { $TimespanFormat -f (New-TimeSpan -Seconds ($_.Average/1000)) } } } if ($ResultOfmigrate.warnings.Count -gt 0) { $ResultOfmigrate.warnings | format-table } $ResultOfmigrate | gm -MemberType NoteProperty | #turn it into a hash table for easiest display. where { $_.Name -notin @('warnings', 'migrations') } | foreach{ @{ $_.Name = $ResultOfmigrate."$($_.name)" } } | format-table } } <# if this didn't work for some reason, we decide to quit #> if ((!($ThisWentWell)) -and $params.OnError -eq 'Abort') { break; } } |
Conclusions
Here we have the bones of a system that will scale easily but can be used to just test a migration. In a way it is a shame to have to execute Flyway from PowerShell, but it adds a lot of flexibility to a Flyway migration and allows you to do a lot more. This is intended to be a simple introduction to the idea of specifying several databases for a Flyway migration. Although I find it saves me time, it can get its real power by running integration tests so that it not only tests that the migration works, but also that it does what it is supposed to do. We’ll have to leave that for another article!
Tools in this post
Flyway Teams
Ideal for organizations looking to improve collaboration and fine tune their processes during development and the deployment of database changes.