Scripting Databases with Flyway Enterprise CLI
This article will cover the basics of the Flyway generate command and how it can auto-generate several types of Flyway migration scripts. This includes versioned migrations that, after testing, can be used to deploy changes, and baseline migration and undo scripts that are useful for a range of development tasks.
Flyway Enterprise CLI’s generate
command automates script generation, making it easier to produce the scripts needed to support the deployment process, and helping developers generate the scripts they need for various development tasks. This might be undoing branch changes, getting a build script for a particular version, producing a script to merge branches without conflicts, or simply just preserving work-in-progress changes.
Previously, integrating SQL Compare with Flyway required additional scripting due to their differing approaches to database connectivity. With schema comparison capabilities built directly into Flyway CLI, generating versioned, undo, and baseline migration scripts is much more straightforward.
The different types of Flyway scripts and their uses
Migration scripts are central to a Flyway database development, but other scripts become more necessary as databases grow. Typical examples are baseline scripts and undo migration scripts. Unlike versioned migration, these merely support the process. Whenever possible, they ought to be generated automatically as development proceeds. Some of these scripts, such as the undo scripts or the migration used to merge a branch back into its parent, are tedious to do manually and error-prone.
Versioned migrations
A versioned migration (V) script takes the database from a previous migration to the next, applying a discrete set of changes (e.g., adding a column, creating a new table) and is tied to a specific version in Flyway’s versioning system. These scripts are executed sequentially to apply changes to the database schema or data.
When working in a branch, you can use Flyway to retrospectively capture changes to a development database into a migration script, whether the changes are made via SSMS or using an ER Modelling tool or a table builder. We can do much more, besides. For example, by comparing the model of the database created at the point of branching with the current state of the branch database, Flyway will generate a script capturing only the changes made within the branch. This can then form the basis of a script to merge a feature branch back into its parent after we’ve checked when the interim changes in the parent branch to see if there are any collisions.
Undo scripts
An undo (U) script aims to revert to the previous version, leaving no trace in the metadata that it happened. The script defines how to reverse the changes introduced in a migration, such as dropping objects created by the migration or restoring modified data. Ideally, you’d want to have an undo script for every version, because if there is a complete chain, we can do undo runs down any number of versions.
Undo scripts are always useful in branch work for continuously making, testing, and reverting changes and I’ve been using Redgate comparison tools for years to provide them.
Baseline migrations
The Baseline migration (B) script is very similar to a build script for a particular version of the database. Starting with an empty database, it will migrate it to the version specified in the file name, creating the database schemas where necessary and then all the database objects, in the correct dependency order. I’ve explained the main uses for this script, and how they work, in Flyway’s Baseline Migrations Explained Simply.
Baseline migrations have a use as a reference even if they’re not used as a script. They are a good way of understanding the overview, and the object inter-relationships. With a baseline migration for each version, teams that prefer can review the changes made between any two versions by viewing the textual differences through the version control system.
However, the same information is available using Flyway’s DiffText
command on the diff artefact, with the added attraction that only the objects that have differences are listed.
Creating the diff artefact
I’ve already described how to use the diff
command to compare databases and generate a diff artefact in Comparing Databases and Generating Schema Models with Flyway. This is merely a machine-readable report of the differences between the source and target. A Source or target can be a database, a directory of object-level build scripts called a schema model, a snapshot, or a list of directories containing the migration files. It is generally tidiest to use snapshots to represent any database that doesn’t currently exist, though models are just as good.
Generating the Scripts
Once we have the diff artefact, we can then use the generate
command to produce versioned, undo, and baseline scripts. We can leave the details to Flyway or specify such matters as the type of file, the filename, or the file location. There are plenty of options, depending on what we wish to accomplish.
generate.target
– the target for which the script will be generated This must be either the source or the target used in the current diff artefact. If you specify nothing, it uses thediff.target
generate.changes
– used when you need to specify those changes listed in the DIFF artefact that you want the script for. To specify them, you must prepare a comma-separated list of change IDs, gleaned from the Diff Artefact by using theDiffText
command. If nothing is specified, then all changes are scriptedgenerate.types
– This will be a list of the types of script you want to be generated. All the types of script that you want to be generated should be presented as a comma-separated list of script types, being one of versioned, undo or baseline. If you don’t specify otherwise, just a versioned script is generated (prefix V).generate.version
– the version part of the migration name to be used in the generated script. If you don’t specify it, Flyway will calculate it, where necessary, to be the next migration version if a versioned migration is specified. Otherwise, it will use the current version. This is not needed if you specify thebaselineFilename
,undoFilename
orversionedFilename
generate.description
– this specifies the ‘description’ part of the migration’s filename to be used in the generated script. If nothing is specified, there will be no description unless you specify thebaselineFilename
.generate.baselineFilename
– if you have the filename (or full path) to use for the generated baseline migration. This cannot be used if you specify the description & version.generate.versionedFilename
– the filename (or full path) to use for the generated versioned migration. This cannot be used if you specify description & version.generate.undoFilename
– the filename (or full path) to use for the generated undo migration. This cannot be used if you specify description & version.generate.location
– the location to place the generated migration, specified as a path..generate.artifactFilename
– the location of the diff artefact to apply to the target. Defaults:diff.artifactFilename
or%temp%/flyway.artifact.diff
.generate.addTimestamp
– adds a timestamp to the calculated version if one is not already present. Default: falsegenerate.force
– deletes any existing file of the same name in thegenerate.location
you specify. Otherwise, you get an error if the file already exists
As a simple example, here’s a PowerShell script that provides you with a baseline migration from the main database. For illustration, I’ve made details of the main environment explicit, in an array, but generally, you’d read the environment from a TOML configuration file.
1 2 3 4 5 6 7 8 9 10 |
$Main=@('-environments.main.url=<the URL>', '-environments.main.user=<username>', '-environments.main.password=<password>', '-environments.main.schemas=<list of schemas>', '-environment=main' ); flyway $Main diff '-diff.source=main '-diff.target=empty' ` generate "-generate.types=baseline" '-generate.description=Generated'` "-generate.location=<mylocation>\scripts" ` "-generate.force=true" |
Workflows for auto-generating Flyway scripts
In my experience, the approach you use for Flyway development will vary with the type of project, size of project, and stage of a project. Whichever one you choose the scripting facility in Flyway Enterprise can automatically generate whatever scripts you need to support it.
The Flyway Desktop workflow
If you use the Flyway Desktop GUI, then you will be familiar with a workflow where changes made to a local development database are saved to the schema model, which is then compared to the latest releasable version, represented by the migrations folder, to generate a versioned migration script. This workflow can now be automated using the Flyway CLI, as described in detail by Tonie Huizer in Automating Flyway Desktop Development using the Flyway CLI.
A migration-first workflow
Flyway will adapt to a range of development workflows. My preferred approach is ‘migration first’, which assumes that only the migration chain contains the true definition of the database version. I typically maintain one Flyway-managed database per branch. I generate a first-cut of each versioned (V) and undo (U) migration, testing and refining the V script repeatedly, using the U script to reset the branch each time. In this way, I can ensure clean, reliable migrations before committing them to the migration or undo chains. This ‘cautious’ approach can be especially relevant for any migration where, for example, the schema changes require additional logic to migrate and preserve existing data.
When Flyway runs the versioned migration, officially recording the version in its schema history table, I generate and save a range of scripts and artifacts that record what’s in that version and the changes applied to produce it. As a routine, I treat these as artifacts and save them separately, for each version of the database.
– Autogenerating scripts in a callback
In team-based development with branching, I like to take a structured approach that uses an afterMigrate
callback to automatically save the necessary artifacts for each version at the point when Flyway registers the database at that version. These artifacts (generated scripts, schema models, or snapshots) are stored in per-version subfolders within the project directory. The schema version number is extracted from Flyway’s info
command output (JSON format) and used to name the artifact files, ensuring that they are always tied to the correct version.
I’ll present the full callback script in another article, but here I’ll show a more bare-bones script you’d run after Flyway runs migrate. This script can be used to auto-generate a forward migration (versioned), reverse migration (undo), and build (baseline migration) for each version.
As described earlier, the undo scripts are immediately useful for branch work or if you are the only developer. The baseline migration can be useful if you ever use this version as a baseline. The forward (V) migration script is only included for completeness, though it can be useful if you wish to save speculative work beyond the current version, or in branch-based development, where migration files tend to get messy due to frequent alterations.
– The worked example
The example will run in the example Pubs database. A sample Flyway project for Pubs is available in my Flyway Teamwork PowerShell framework on GitHub. This framework uses a directory system that supports branching and merging files, but it should be easy to edit it for your own setup. You only need the versions directory, and the individual per-version subdirectories are created for you.
The script generates V and U migration scripts by creating a snapshot for the current version and comparing it against a snapshot of the previous version. It assumes the previous snapshot exists in a standard location (Reports/Snapshot.json) so you’ll need to create the initial snapshot (e.g. of an empty database). The Flyway commands are simple. The work is in gathering credentials and fetching the name and version number of both the current version and the previous version. You need both.
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 95 96 97 98 99 |
$ErrorActionPreference = 'Stop' #whatever error happens, we dont want to continue <# The objective here is to allow you to run this without altering your existing project TOML file and to avoid real connection information getting beyond your user area I have, for every database type, project, branch, and server, a file with connection information in it that can be used. It uses the 'current' environment. The Filenames go Project_RDBMS_Branch_Server such as ... C:\Users\Phil\Pubs_SQLServer_Main_Philf01.toml typical contents are .... flyway.environment = "current" [environments.current] url = "jdbc:<My Connection information>;" user = "PhilipFactor" password = "DeadSecret" You will want to use your environments and resolvers in flyway.user.toml to do this so you just need to end up with a string array called $current with your credentials #> $Project = 'Pubs' # for working directory and the name of the credentials file $Branch = 'Main' # the branch. We use this just for suitable login credentials $Server = 'Philf01' # $credentialsFile = "SQLServer_$($Branch)_$Server" $ProjectArtefacts = ".\Versions" # the directory for artefacts #we specify where in the user area we store connections and credentials $currentCredentialsPath = "$env:USERPROFILE\$($Project)_$credentilsFile.toml" #go to the appropriate directory - make it your working directory cd "$env:FlywayWorkPath\$Project" $CurrentCredentialsPath = "$env:USERPROFILE\$($Project)_$credentilsFile.toml" <# $current just contains your 'current' environment. If you call it something different, you'll need to change it in the subsequent code #> $current="-configFiles=$CurrentCredentialsPath" # we establish what the current version of the database is $Info =(flyway $current '-outputType=json' info) | convertFrom-JSON if ($Info.error -ne $null) # we must check for an error in INFO { write-error $Info.error } $Version = $Info.schemaVersion # get the current version no #Make sure that all the basic directories are there If (!(Test-Path -Path "$ProjectArtefacts")) #if there is no versions directory { md "$ProjectArtefacts" } # make sure the versions directory is there $ListOfVersions = $Info.migrations.rawversion | where { $_ -ne '' } $ListOfVersions | foreach { # and all the version directories If (!(Test-Path -Path "$ProjectArtefacts\$_")) { md "$ProjectArtefacts\$_" } } # find the path of the snapshot of the previous version in the versions directory <# (These directories need to be done in order so that there is a previous version complete with its Snapshot) #> $description=$Info.migrations|where {$_.rawVersion -eq $version}|foreach {$_.description} $PreviousVersion = $ListOfVersions | where { [version]$_ -lt [version]$Version } | Sort-Object -Property [version]$_ -Descending | Select -First 1 #Calculate where the scripts for the current version should go $CurrentArtefactLocation = "$ProjectArtefacts\$Version\Reports" # $CurrentScriptsLocation = "$ProjectArtefacts\$Version\Scripts" #...and where the Snapshot for the previous version should be $PreviousSnapshotLocation = "$ProjectArtefacts\$PreviousVersion\Reports\Snapshot.json" #make sure an RG Snapshot is there if (!(test-path -path "$PreviousSnapshotLocation" -PathType Leaf)) { Write-error "there is no Snapshot in $PreviousSnapshotLocation that we can compare with, sadly" } flyway snapshot $current '-snapshot.source=current' ` "-snapshot.filename=$CurrentArtefactLocation\Snapshot.json" $DiffParams = @( "-diff.source=snapshot:$CurrentArtefactLocation\Snapshot.json", <# The source to use for the diff operation. 'empty', 'schemamodel', 'migrations' or the name of an 'environment' #> "-diff.target=snapshot:$PreviousSnapshotLocation", '-outputType=json', "-diff.artifactFilename=$CurrentArtefactLocation\artifact.diff" ) flyway $current $Diffparams diff >"$CurrentArtefactLocation\Differences.json" # this is done because the list of types doesn't work as advertised @('undo', 'versioned') | foreach { #'versioned' left out until name correct $GenerateParams = @( "-generate.artifactFilename=$CurrentArtefactLocation\artifact.diff" "-generate.types=$_", "-generate.version=$Version", "-generate.description=Generated-$description", "-generate.location=$CurrentScriptsLocation", '-generate.force=true' ) # # Generate the versioned and undo scripts, overwriting any existing files flyway $current $GenerateParams generate -outputType=json > GenerateResult.json Type GenerateResult.json | convertFrom-JSON | foreach{ if ($_.error -ne $null) { write-Error $_.error.message } if ($_.warnings.count -gt 0) { $_.warnings | foreach{ write-warning $_ } } } #Del GenerateResult.json } # now do the baseline script. This requires a Diff with empty, # otherwise it is identical to the versioned file # this works flyway $current diff '-diff.source=current' '-diff.target=empty' ` generate "-generate.types=baseline" '-generate.description=Generated'` "-generate.location=$CurrentScriptsLocation" ` "-generate.force=true" Copy-Item $CurrentArtefactLocation\scripts\*.sql ` -Destination "$ProjectArtefacts\current\scripts" -Force |
Conclusion
These scripts can make life easier during development, not only in development to have free undo scripts for every version, but for rapidly looking through the CREATE statements of each version to see what is in the database, or to see what is changed. Although the code that is generated is functional rather than immediately intelligible, it is handy to have, particularly with a DIFF tool. The more people there are in the team, the more useful it becomes. It is very good for bug post-mortems because one can see quickly how and when a bug was introduced.
I’ve been generating scripts with Flyway for years. Before Flyway provided native comparisons, I used the separate SQL Compare and Oracle Compare UI tools in CLI mode to provide the same feature. It was a great help. The problem was that ODBC and JDBC are incompatible database interfaces, so combining Flyway (JDBC) and SQL Compare (ODBC) inevitably meant having to massage the credentials.