Customizing Database Deployments using Flyway Callbacks and Placeholders
How to customize a database deployment process using Flyway, demonstrating how to incorporate tasks such stamping a version number into the latest database build, or writing to the SQL Server log.
Any way of automating a database development process must meet a wide range of requirements and practices. It is no good for a tool to require a development team to change the way it works, but it is fine for a tool to allow it to do so. Flyway gains its versatility from the ease with which the process of building a database can be customized. By incorporating use of SQL callback files, and placeholders, into your database automation scripts, you can add some magic to the process. This article gives a practical illustration of how that can happen.
For a general introduction to Flyway for the developer, see Managing database changes using Flyway: an Overview and for a simple set of demonstrations of how it might work, see Getting Started with Flyway and SQL Server. For all current and forthcoming Flyway articles, see Flyway Content Map.
What are SQL callbacks and placeholders?
SQL callbacks are SQL Files that are outside a specific database migration step but are triggered in Flyway by most events in the process, such as before or after a migrate
command runs, or at the end of the entire process, or if an error occurs anywhere within it. You can specify that the SQL file is called before or after almost any type of event (start, error or completion) and for every action (Baseline, Clean, Info, Migrate, Repair, Undo and Validate).
Callbacks are often used before a migration (‘beforeMigrate‘ callbacks) to do checks or initialization work, and after a migration (‘afterMigrate‘ callbacks) to perform tasks such as setting access controls or loading data or recording in the log that the migration has taken place. You can set callbacks for every file or statement in a repeatable, migration or undo file, in response to an event such as an error or successful completion. There are very few events that don’t allow a callback.
Flyway uses Placeholders to allow us to specify values that can be used by any executable file. They perform the same role as ‘macros’ in traditional tools, and the scripting variables in SQLCMD. Before Flyway executes a SQL File, it looks for placeholders and will swap them out for the run-time values before the code is executed.
There are a few built-in placeholders that you can use to pass important information to any callback or script, such as the date, the database name, location of the flyway database and the username. You can also create your own custom placeholders, just by specifying them in a config file, environment variable or command line parameter. This allows you to provide parameters for the callbacks.
The way you use placeholders will vary according to the way your development team works. You can use them to prevent or allow sections of code being executed, or more likely to have a particular variant of code executed. Some databases, for example, have different schemas, table-suffixes, or even different column names, for different variants of a version. You can also use them within scripts, such as callbacks, to allow them to send database messages or alerts to the correct destination.
In this article, I’ll build on the example that we developed for the previous articles in this series, building a sort of super-Pubs database (publications, not public houses, sadly). I’ll show you a couple of examples of using callbacks and placeholders in combination.
Attaching a version stamp directly to a database
To detect the version of a database that has been built by Flyway, you must use Flyway itself, or interrogate the flyway_schema_History
table. This is by design because Flyway is designed to work with many different database systems. However, SQL Server has extended properties that can be attached directly to databases. Many DBAs prefer to use these because they contain much more information, can be read more easily, and require fewer access permissions. So. why not have both?
Although the MS_Description
key for the database is generally used to tell us about the version, and is what Redgate SQL Doc uses, the format isn’t standardized across tools. To write to the extended properties we generally call a SQL Routine that either creates or updates the property when the version changes. Flyway allows us to provide the routine, just once, as a callback, after a migration run has completed. Flyway will read the start of the filename to work out when we want it to be called. All we need to do is to start the filename with ‘afterMigrate‘ and Flyway will execute the SQL file after it has completed a migration run.
The following ‘afterMigrate‘ callback file will add or update an extended property (Database_info
) on the target database recording the final version of the database.
When the file is executed, the script will be able to determine from SQL Server metadata functions the name of the database and the user, so it just needs the name of the version. Unfortunately, there is no placeholder for this. Flyway writes the final version to the flyway_schema_History
table before this ‘afterMigrate‘ callback gets executed, so we can pick up the correct version from there. We just need to know th table’s location in the target database, and we can find the user’s default schema using another built-in placeholder (flyway:defaultSchema
).
Here are the contents of the file:
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 |
/* we start by writing out the database name, the project name, the version and the time it happened to a JSON document stored as an Extended Property directly against the database. This allows DBAs to easily generate lists of all the development databases on the database, what development projects they belong to, the version, and when they were last updated. */ DECLARE @Version VARCHAR(20); -- the database version after a migration run. --the only reliable way of finding the version as far as I know is to use --the flyway table. This could be stored in any schema so we need to determine --where the user is storing this table- via a built-in placeholder SELECT @Version=[version] --we need to find the greatest successful version. FROM ${flyway:defaultSchema}.flyway_schema_History -- WHERE installed_rank = (--get the PK of the highest successful version recorded SELECT Max(Installed_Rank) FROM ${flyway:defaultSchema}.flyway_schema_History WHERE success = 1 ); --first, we create the JSON document, allowing us to easily get --all the object properties. PRINT N'Recording the database''s version number - '+@version; DECLARE @Database NVARCHAR(3000); SELECT @Database = N'${flyway:database}'; DECLARE @DatabaseInfo NVARCHAR(3000) = ( SELECT @Database AS "Name", @Version AS "Version", N'${projectDescription}' AS "Description", N'${projectName}' AS "Project", GetDate() AS "Modified", SUser_Name() AS "by" FOR JSON PATH ); --now we either update or create the EP IF NOT EXISTS --does it exist? ( SELECT fn_listextendedproperty.name, fn_listextendedproperty.value FROM sys.fn_listextendedproperty( N'Database_Info', DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT ) )--Oooh no. So we create it EXEC sys.sp_addextendedproperty @name = N'Database_Info', @value = @DatabaseInfo; ELSE -- yes, upodate it EXEC sys.sp_updateextendedproperty @name = N'Database_Info', @value = @DatabaseInfo; |
You’ll see the placeholders. By default, they start with a dollar sign and curly brace (${
) prefix and end with a curly brace (}
) suffix, but you can change that by specifying what you want to use instead, in the config file. You can specify the prefix 'flyway.placeholderPrefix='
and the suffix using 'flyway.placeholderSuffix='
. SQL Server die-hards will want to stick to the sqlcmd
convention of using a ‘$(
‘ prefix and ‘)
‘ suffix so that their files can be used by both sqlcmd
and Flyway, and they can test out Flyway callbacks using SQLCMD, or SSMS in SQLCMD mode.
You reference the default placeholders using ${flyway:placeholder}
, whereas custom placeholders just use ${placeholder}
. These placeholders correspond with your Config file settings, but with the colon (:
) instead of a dot (.
).
The above callback file has two custom placeholders, ${projectName}
and ${projectDescription}
which I can either specify in the command-line, (-placeholders. myplaceholder =”Value”
) an environment variable (FLYWAY_PLACEHOLDERS_MYPLACEHOLDER=value
), or in one of the Config files (flyway.placeholders.myplaceholder=value
).
You can even switch placeholders on and off (default: true
) using the 'flyway.placeholderReplacement='
setting. If you set it to false
, the placeholder values won’t be swapped in.
Save the SQL Callback file as afterMigrate__Add_Version_EP.sql and place it in in the Scripts subdirectory of the project directory PubsAndFlyway\PubsFlywaySecondMigration\Scripts. I’ve left an example in the GitHub project. Be careful to make it a UTF-8 file and remember too that Flyway uses case-sensitive filenames for compatibility with the Linux environment.
Now we just run a migration using Flyway, which I’ve described how to do in the previous articles. This time, we’ll take Pubs
from zero up to version 1.1.3 and then see if the extended property has been set as we expect.
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 |
#create an alias for the commandline Flyway, Set-Alias Flyway 'C:\ProgramData\chocolatey\lib\flyway.commandline\tools\flyway-7.3.1\flyway.cmd' -Scope local # and here are our project details. The project folder $ProjectFolder = 'OurPathTo\PubsAndFlyway\PubsFlywaySecondMigration' $Server = 'OurServer' $Database = 'PubsFly'; $ProjectName='Publications'; $ProjectDescription='A sample project to demonstrate Flyway, using the old Pubs database' <# you only need this username if there is no domain authentication #> $username = 'MyID' $port = '1433' # 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"} # now we get the password if necessary if ($username -ne '') #then it is using SQL Server Credentials { # we see if we've got these stored already $SqlEncryptedPasswordFile = "$env:USERPROFILE\$($username)-$SourceServer.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 $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. #> } $Uid = $SqlCredentials.UserName; $Pwd = $SqlCredentials.GetNetworkCredential().password $FlyWayArgs = @("-url=jdbc:sqlserver://$($Server):$port;databaseName=$Database", "-locations=filesystem:$ProjectFolder\Scripts", <# the migration folder #> "-user=$($SqlCredentials.UserName)", "-password=$($SqlCredentials.GetNetworkCredential().password)") } else { $FlyWayArgs= @("-url=jdbc:sqlserver://$($Server):$port;databaseName=$Database;integratedSecurity=true". "-locations=filesystem:$ProjectFolder\Scripts")<# the migration folder #> } $FlyWayArgs+= <# the project variables that we reference with placeholders #> @("-placeholders.projectDescription=$ProjectDescription", "-placeholders.projectName=$ProjectName") <# the project variables #> Flyway clean $FlyWayArgs # remove all objects from the database, including baselines flyway info $FlyWayArgs # check that it has the correct plan for executing the scripts Flyway migrate $FlyWayArgs '-target=1.1.3' # Run up to v1.1.3 flyway info $FlyWayArgs #check that it all worked |
When we execute this, we get the normal burst of messages, and hopefully at the end we get this encouraging news…
And if we use SSMS we see:
Writing to the SQL Server Log
In this example, we’ll use a callback to log a Flyway migration in a SQL Server database so that it can be checked by a DBA or picked up by a monitoring system, such as SQL Monitor. The statement that logs the build must be executed after a deployment. It can’t be put in any particular migration file because it must be executed just once, at the end of the migration process, which could involve several migration files. We also can’t put it in every file because it is quite usual to have a migration that includes more than a hundred files, which would be bad news for the log file. We therefore use a callback.
1 2 3 4 5 6 7 8 9 10 |
-- This next statement writes to the SQL Server Log so that the event is recorded -- in the server's log, and if an event is defined, an alert can send a notification -- and tools such as SQL Monitor can show this deployment. --Do I, the user, have permission on this securable I want to use? IF HAS_PERMS_BY_NAME(N'sys.xp_logevent', N'OBJECT', N'EXECUTE') = 1 BEGIN Declare @eventMessage AS nvarchar(2048) SET @eventMessage = N'Flyway deployed to ${flyway:database} to version '+@version+' on ${flyway:timestamp} as part of ${projectName}' EXECUTE sys.xp_logevent 55000, @eventMessage END |
We add this to the previous SQL callback file (afterMigrate__Add_Version_EP.sql) and run the migration again.
If you then check the current SQL Server error log, you’ll see our deployment message:
Conclusions
Placeholders and callbacks don’t necessarily have to be used together. Callbacks provide a powerful way to prevent you having to duplicate code, and to manage aspects of the build, such as the insertion of data, server and database-level configuration and access control, that aren’t normally considered part of the development schema. Placeholders provide a powerful way of reporting, and of producing variants of a version as when a database must have special procedures for each legislative area.
This is typical of Flyway. It has evolved to meet the diverse needs of a large community of developers, who are using many different tools, sometimes in surprising ways. Flyway aims to fit in with the flavor of database being used, and the development build tools that are being used to release software. It illustrates the real ‘DevOps’ approach of allowing tools to interact and work together by being versatile, accommodating and above all, configurable!