Flyway and SSDT: Extracting a DACPAC from a Flyway-managed Database
When you are integrating Flyway into an existing SQL Server SSDT development, you don't necessarily have to change everything at once. The development team might continue to use the SSDT tools, but Flyway will soon take over the deployments. This means that any automated processes will need to be able to handle both DACPACs and Flyway migration scripts with equal grace. In this article, I'll demonstrate how to automatically extract a versioned DACPAC from each new Flyway version of a database.
In an SSDT-Flyway ‘hybrid’ system, I’d recommend that the individual development of the database code continues without disruption, whereas the merging of all this work into the development database, and all subsequent deployments, is done via a Flyway migration script. This quickly delivers the many advantages of Flyway’s versioning system to team development.
Each time Flyway successfully produces a new version, we can automatically generate DACPACs along with the various build artifacts that make team-based development simpler, including a build script to reproduce the new version and object-level source to verify a version and to track and investigate changes. A DACPAC is used as part of a process that allows isolated branch development work to keep up to date with other committed changes.
In this article, I’ll provide a PowerShell task to auto-extract a ‘versioned DACPAC’ from each new Flyway version. We use this task in a Flyway callback script, so that it runs automatically on the successful completion of any Flyway migration run. I’ll even demonstrate how to retrospectively produce DACPACs for older versions of the database. Teams can use these DACPACs to recreate the new version locally for local development work or upgrade older versions.
In a subsequent article, I’ll demo how to extract a Flyway-compatible migration script from a DACPAC, for use in merge operations and subsequent deployments.
Provisioning databases using DACPACs
Although they are a neat way of packaging a database, DACPACs aren’t cross-RDBMS technology. They are like an insect caught in aspic or resin, complete but lifeless, a database at a moment in time. Unlike the insect, a SQL Server database caught in a DACPAC can be revived and turned back into a functioning database just as long as all the external databases, CLR libraries, server-references and other dependencies can also be revived. As such, DACPACs are handy. You can, of course, ‘script out’ the database and possibly also the data, in a compressed form, but a DACPAC can be written or read by any SQL Server tool such as SSMS.
I tend to create DACPACs as a matter of routine (or ‘extract’ them, in the SSDT terminology). By default, we store data separately, in compressed BCP files on the server’s filesystem. However, if this is not possible, you can include data in the DACPAC, though it is likely to get quite large. You can also store the version, though it is not attached to the database in SSDT but to the server.
A team member can do an isolated install of the database from a DACPAC to try things out, as well as extract either a build script or an object-level source. A DACPAC can be ‘published’ to update a database schema to match the schema contained in the DACPAC file. If the database does not already exist on the server, the publish
operation creates it. Otherwise, it is updated. This is done via a generated Synchronization script called an ‘incremental update’ script. You can either create the script, inspect it and apply it using SQLCMD.exe, or if you are feeling lucky, you can leave it to SSDT (DacFx) to do it for you.
If we create a DACPAC of a Flyway database and use that DACPAC to publish to a target database, the published target database will have a Flyway Schema History table copied over as well. If we include data in the publishing operation, the target database will be at the version of the source of the DACPAC. If you don’t want this, it will need to be deleted after the publish
operation is completed.
Creating a DACPAC from the current Flyway version
I have a PowerShell Script block task ($ExtractFromSQLServerIfNecessary
) that I use for extracting a DACPAC. It uses SQLPackage.exe, which is part of the SSDT tool pack. It can be used ‘isolated’ or within my Flyway Teamwork framework. Within the framework, assuming use of Flyway Teams or Enterprise Edition, it will work in a PowerShell callback so can be set to produce a DACPAC for each version automatically. It only needs to create the DACPAC once per version, so it checks first. You can override this behavior.
Here is an example of calling it in isolated mode, so you can try it out without using the Framework. Because it isn’t sharing details with a whole chain of processes, it looks a bit messy, but you wouldn’t see this when using the Script-block with the framework, because all the Script-blocks share parameters that are there already.
1 2 3 4 5 6 |
Process-FlywayTasks @{ 'version'='1.1.5'; 'server'='MyServer'; 'reportLocation'=MyDirectory; 'database'=' MyDatabase '; 'pwd'='mySecretPassword'; 'uid'='MyUserID'; 'project'='Pubs'; 'projectDescription'='A simple Demonstration'; 'problems'=@{};'warnings'=@{};'feedback'=@{};'writeLocations'=@{} } $ExtractFromSQLServerIfNecessary |
Executed ExtractFromSQLServerIfNecessary in ExtractFromSQLServerIfNecessary, Connecting to database 'MyDatabase' on server 'myServer'. Extracting schema Extracting schema from database Resolving references in schema model Validating schema model Validating schema model for data package Validating schema Exporting data from database Exporting data Processing Export. Processing Table '[dbo].[flyway_schema_history]'. Processing Table '[dbo].[employee]'. Processing Table '[dbo].[jobs]'. Processing Table '[dbo].[stores]'. Processing Table '[dbo].[discounts]'. Processing Table '[dbo].[publishers]'. Processing Table '[dbo].[pub_info]'. Processing Table '[dbo].[roysched]'. Processing Table '[dbo].[sales]'. Processing Table '[dbo].[authors]'. Processing Table '[dbo].[titleauthor]'. Processing Table '[BookQuery].[flyway_schema_history]'. Processing Table '[BookQuery].[PhraseBanks]'. Processing Table '[dbo].[publications]'. Processing Table '[dbo].[editions]'. Processing Table '[dbo].[prices]'. Processing Table '[dbo].[TagName]'. Processing Table '[dbo].[TagTitle]'. Processing Table '[dbo].[Publication_Types]'. Successfully extracted database and saved it to file <MyDirectory>\Pubs1.1.5-DACPAC.DACPAC'. Time elapsed 0:00:44.60 Written DACPAC for Pubs 1.1.5 to <MyDirectory>\Pubs1.1.5.DACPAC
Here is the actual Script-block. It is messier than you might imagine because I deal with feedback, verbose information and warnings. I also check the inputs fairly thoroughly. You’ll notice that it gives you full access to all the ‘extract’ functions, including producing build script files and all the possible types of object-level directories. That makes it a lot easier to commit your work to source control in whatever format it requires. Flyway Teams and Enterprise Editions can read these object directories very easily, making integration smoother.
The current source for this is in the resources directory on GitHub, within the file DatabaseBuildAndMigrateTasks.ps1. You’ll also need the Process-FlywayTasks
function that is included there.
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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
$ExtractFromSQLServerIfNecessary = { <# this connects to the SQL Serverdatabase and will then, For the current version of the database extract either a 'DACPAC' (output a .DACPAC single file). 'Flat' (all files in a single folder), 'SchemaObjectType' (files in folders for each schema and object type), 'Schema' (files in folders for each schema), 'ObjectType' (files in folders for each object type), 'Flat' (all files in the same folder) 'File' (1 single file). #> Param ($param1, $OutputType, <# {DACPAC|File|Flat|ObjectType|Schema|SchemaObjectType} #> $RedoIt = $false, #by default you just do it the once $doDiagnostics = $false # do you want the log saved to disk to see what went wrong? ) # $ExtractFromSQLServerIfNecessary (Don't delete this) $problems = @(); # well, not yet (all problems are returned in this array) $feedback = @(); # well, nothing yet (all feedback is returned in this array) if ($doDiagnostics -eq $null) { $doDiagnostics = $False } if ($OutputType -notin ('DACPAC', 'File', 'Flat', 'ObjectType', 'Schema', 'SchemaObjectType')) {# just assume that if the output type is wrong, the user wanted a DACPAC. $OutputType = 'DACPAC'; # 'DACPAC' } #check that we have values for the necessary details @('version', 'server', 'reportLocation', 'database', 'pwd', 'uid', 'project', 'projectDescription') | foreach{ if ($param1.$_ -in @($null, '')) { $Problems += "no value for '$($_)'" } } <# has the user installed SQLPackage? - or specified the path as a variable#> $command = get-command sqlpackage -ErrorAction Ignore if ($command -eq $null) { if ($SQLPackageAlias -ne $null) { Set-Alias sqlpackage $SQLPackageAlias } else { $problems += 'You must have provided a path to $SQLPackage.exe in the ToolLocations.ps1 file in the resources folder' } } #the database scripts path would be up to you to define, of course $VersionsPath = if ([string]::IsNullOrEmpty($param1.VersionsPath)) { 'Versions' } else { "$($param1.VersionsPath)" } $EscapedProject = ($Param1.project.Split([IO.Path]::GetInvalidFileNameChars()) -join '_') -ireplace '\.', '-' <# The SqlPackage Extract action creates a schema of a connected database in a DACPAC file (.DACPAC). By default, data is not included in the .DACPAC file. To include data, utilize the Export action or use the Extract properties ExtractAllTableData/TableData. /p:ExtractTarget Specifies alternative output formats of the database schema, default is 'DACPAC' to output a .DACPAC single file. Additional options output one or more .sql files organized by either 'SchemaObjectType' (files in folders for each schema and object type), 'Schema' (files in folders for each schema), 'ObjectType' (files in folders for each object type), 'Flat' (all files in the same folder), or 'File' (1 single file). #> # objectType SchemaObjectType, schema and flat create a directory. File creates a file $ReportDirectory = "$($param1.reportLocation)\$($param1.Version)\"; $OutputFile = "$ReportDirectory$($EscapedProject)$($param1.Version)-$OutputType.DACPAC" $ExtractArguments = @("/Action:Extract", <# /Sourcefile isn't used here, and is commented out. This parameter specifies a source file to be used as the source of action instead of a database. For the Publish and Script actions, SourceFile may be a .DACPAC file or a schema compare .scmp file. If this parameter is used, no other source parameter is valid. #> "/TargetFile:$Outputfile", <# Specifies a target file to be used for the DACPAC #> "/p:ExtractAllTableData=true", "/p:ExtractTarget=$OutputType", "/p:VerifyExtraction=true", "/p:DacApplicationDescription=$($param1.projectDescription)", "/p:DacApplicationName=$($param1.database)-[$($param1.version)]", "/SourcePassword:$($param1.pwd)", <# For SQL Server Auth scenarios, defines the password to use to access the Source database. (short form /sp) #> "/SourceServerName:$($param1.Server)", <# Defines the name of the server hosting the source database. (short form /ssn) #> "/SourceDatabaseName:$($param1.database)", <# Specifies an override for the name of the database that is the source of SqlPackage.exe Action. (short form /sdn) #> "/SourceUser:$($param1.uid)"<# For SQL Server Auth scenarios, defines the SQL Server user to use to access the source database. (short form /su) #> "/SourceTrustServerCertificate:true" ) if ($DoDiagnostics) { $ExtractArguments += ` "/DiagnosticsFile:$ReportDirectory$($EscapedProject)$($param1.Version)$OutputType.log " } <# a bug in SQLPackage means that it always has a DACPAC filetype even when that isn't so. We calculate what the file name should be once we execute SQL Package and will change it! #> $NewOutputType = switch ($OutputType) { 'File' { 'SQL' } default { $OutputType } } $ChangedOutput = $outputFile.Replace("-$OutputType.DACPAC", ".$NewOutputType") if ($ChangedOutput -eq $OutputFile){$Feedback+="$ChangedOutput is unchanged from $OutputFile"} $AlreadyDone = (Test-Path $ChangedOutput) if ($RedoIt -eq $false -and $AlreadyDone -eq $true) { $Feedback += "The $OutputType has already been created for $($EscapedProject) $($param1.Version)" } else { <# deal with feedback #> if ($problems.Count -eq 0) { if (-not (Test-Path "$ReportDirectory" -PathType Container)) { New-Item -ItemType directory -Path "$ReportDirectory" -Force } else { if (Test-Path $OutputFile) { Remove-item $OutputFile } } #Now we actually execute SQL Package. $console = sqlpackage $ExtractArguments | foreach{ "$_ `n" } $Feedback += "$console" if ($?) # if no errors then simple message, otherwise... { if (Test-Path $ChangedOutput) { Remove-item $ChangedOutput } rename-item $outputFile $ChangedOutput $Feedback += "Written $OutputType for $EscapedProject $($param1.Version) to $ChangedOutput" } else { #report a problem and send back the args for diagnosis (hint, only for script development) $Problems += "SQLpackage Went badly. (code $LASTEXITCODE) with paramaters $ExtractArguments" } } } if ($problems.count -gt 0) { $Param1.Problems.'ExtractFromSQLServerIfNecessary' += $problems; } if ($feedback.count -gt 0) { $Param1.feedback.'ExtractFromSQLServerIfNecessary' = $feedback } } |
Auto-creating DACPACs for every migration with a Flyway callback
The advantage of running this task within the Teamwork framework is that we can now create a DACPAC for every version of a Flyway project. We then have a canteen for developers who need to quickly create a database at a particular version, even on an isolated database server.
We just put the code in an AfterMigrate
callback and put the path to the callback in the list of locations
paths in the project-level flyway.conf file. Here is a callback script that will do the trick::
1 2 3 4 5 6 |
. '.\preliminary.ps1' $PostMigrationTasks = @( $GetCurrentVersion, #checks the database and gets the current version number $ExtractFromSQLServerIfNecessary #save this version as a DACPAC if it hasn't already been done ) Process-FlywayTasks $DBDetails $PostMigrationTasks |
Creating DACPACs for older database versions
What if you’d like all your old versions to have associated DACPACs? Well, if you don’t mind doing a rebuild, you can use this technique, which just does a single migration on every iteration, and follows that by writing the resulting DACPAC to the Versions folder.
This is pretty draconian. Do not do this to your main development database without lots of backups and checking with the rest of the team. If you’re on your isolated workstation with a copy of the development, then you can be more relaxed.
This routine will save a script directory, a full build script and a DACPAC after every version. That way, you can be prepared for almost any source control requirement, search, bug-hunting operation, or scripting requirement.
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 |
cd '<MyPathTo>FlywayTeamwork\Pubs\Branches\Develop' . '.\preliminary.ps1' Write-Output @" Processing the $($dbDetails.variant) variant of $($dbDetails.branch) branch of the $($dbDetails.project) project using $($dbDetails.database) database on $($dbDetails.server) server with user $($dbDetails.installedBy)" "@ Flyway clean $ExecutedWell = $true; $DbDetails.migrationsLocation | foreach{ Dir "$_\V*.sql" }| foreach{ [pscustomobject]@{ 'file' = $_.Name; 'version' = [version]($_.Name -ireplace '(?m:^)V(?<Version>.*)__.*', '${Version}') } } | where { (!($StartVersion -ne $null -and $_.version -lt $StartVersion) -and !($EndVersion -ne $null -and $_.version -gt $EndVersion)) } | Sort-Object -Property @{ Expression = "version"; Descending = $false } | foreach{ if ($ExecutedWell) { Flyway migrate "-target=$($_.version)" -ignoreMigrationPatterns='*:ignored'; $ExecutedWell = $?; } if ($ExecutedWell) { $DbDetails.version=$_.version; Process-FlywayTasks $DBDetails $GetCurrentVersion <a id="post-6942318-_Hlk132385258"></a>Process-FlywayTasks $DBDetails $ExtractFromSQLServerIfNecessary 'DACPAC' Process-FlywayTasks $DBDetails $ExtractFromSQLServerIfNecessary 'file' Process-FlywayTasks $dbDetails $ExtractFromSQLServerIfNecessary 'ObjectType' } } |
Conclusions
I’ve demonstrated how we can bring DACPAC technology to Flyway. With the code I’ve provided, you should be able to associate a DACPAC with every version. We can now provide DACPACs for anyone who needs them. However, that is only the first stage. What do you do if developers provide a DACPAC from their branch work, and you have to create a migration file from this DACPAC, or maybe even a Flyway UNDO file? Normally, we use SQL Compare for scripting, but there is a snag: it doesn’t read DACPACs directly without help, and the live development database is at a specific version. We’ll tackle this task as a separate exercise.
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.