In my previous article, “Five Tips to Get Your Organisation Releasing Software Frequently” I looked at how teams can automate processes to speed up release frequency. In this post, I’m looking specifically at automating deployments using the SQL Compare command line.
SQL Compare compares SQL Server schemas and deploys the differences. It works very effectively in scenarios where only one deployment target is required – source and target databases are specified, compared, and a change script is automatically generated and applied.
But if multiple targets exist, and pressure to increase the frequency of releases builds, this solution quickly becomes unwieldy.
This is where SQL Compare’s command line comes into its own. I’ve put together a PowerShell script that loops through the Servers table and pulls out the server and database, these are then passed to sqlcompare.exe to be used as target parameters. In the example the source database is a scripts folder, a folder structure of scripted-out database objects used by both SQL Source Control and SQL Compare. The script can easily be adapted to use schema snapshots.
-- Create a DeploymentTargets database and a Servers table
CREATE DATABASE DeploymentTargets
GO
USE DeploymentTargets
GO
CREATE TABLE [dbo].[Servers](
[id] [int] IDENTITY(1,1) NOT NULL,
[serverName] [nvarchar](50) NULL,
[environment] [nvarchar](50) NULL,
[databaseName] [nvarchar](50) NULL,
CONSTRAINT [PK_Servers] PRIMARY KEY CLUSTERED ([id] ASC)
)
GO
-- Now insert your target server and database details
INSERT INTO dbo.Servers ( serverName , environment , databaseName)
VALUES ( N'myserverinstance' , N'myenvironment1' , N'mydb1')
INSERT INTO dbo.Servers ( serverName , environment , databaseName)
VALUES ( N'myserverinstance' , N'myenvironment2' , N'mydb2')
Here’s the PowerShell script you can adapt for yourself as well.
# We're holding the server names and database names that we want to deploy to in a database table.
# We need to connect to that server to read these details
$serverName = ""
$databaseName = "DeploymentTargets"
$authentication = "Integrated Security=SSPI"
#$authentication = "User Id=xxx;PWD=xxx" # If you are using database authentication instead of Windows authentication.
# Path to the scripts folder we want to deploy to the databases
$scriptsPath = "SimpleTalk"
# Path to SQLCompare.exe
$SQLComparePath = "C:\Program Files (x86)\Red Gate\SQL Compare 10\sqlcompare.exe"
# Create SQL connection string, and connection
$ServerConnectionString = "Data Source=$serverName;Initial Catalog=$databaseName;$authentication"
$ServerConnection = new-object system.data.SqlClient.SqlConnection($ServerConnectionString);
# Create a Dataset to hold the DataTable
$dataSet = new-object "System.Data.DataSet" "ServerList"
# Create a query
$query = "SET NOCOUNT ON;"
$query += "SELECT serverName, environment, databaseName "
$query += "FROM dbo.Servers; "
# Create a DataAdapter to populate the DataSet with the results
$dataAdapter = new-object "System.Data.SqlClient.SqlDataAdapter" ($query, $ServerConnection)
$dataAdapter.Fill($dataSet) | Out-Null
# Close the connection
$ServerConnection.Close()
# Populate the DataTable
$dataTable = new-object "System.Data.DataTable" "Servers"
$dataTable = $dataSet.Tables[0]
#For every row in the DataTable
$dataTable | FOREACH-OBJECT {
"Server Name: $($_.serverName)"
"Database Name: $($_.databaseName)"
"Environment: $($_.environment)"
# Compare the scripts folder to the database and synchronize the database to match
# NB. Have set SQL Compare to abort on medium level warnings.
$arguments = @("/scripts1:$($scriptsPath)", "/server2:$($_.serverName)", "/database2:$($_.databaseName)", "/AbortOnWarnings:Medium") # + @("/sync" ) # Commented out the 'sync' parameter for
safety,
write-host $arguments
& $SQLComparePath $arguments
"Exit Code: $LASTEXITCODE"
# Some interesting variations
# Check that every database matches a folder.
# For example this might be a pre-deployment step to validate everything is at the same baseline state.
# Or a post deployment script to validate the deployment worked.
# An exit code of 0 means the databases are identical.
#
# $arguments = @("/scripts1:$($scriptsPath)", "/server2:$($_.serverName)", "/database2:$($_.databaseName)", "/Assertidentical")
# Generate a report of the difference between the folder and each database. Generate a SQL update script for each database.
# For example use this after the above to generate upgrade scripts for each database
# Examine the warnings and the HTML diff report to understand how the script will change objects
#
#$arguments = @("/scripts1:$($scriptsPath)", "/server2:$($_.serverName)", "/database2:$($_.databaseName)", "/ScriptFile:update_$($_.environment+"_"+$_.databaseName).sql", "/report:update_$($_.environment+"_"+$_.databaseName).html" , "/reportType:Interactive", "/showWarnings", "/include:Identical")
}
It’s worth noting that the above example generates the deployment scripts dynamically. This approach should be problem-free for the vast majority of changes, but it is still good practice to review and test a pre-generated deployment script prior to deployment. An alternative approach would be to pre-generate a single deployment script using SQL Compare, and run this en masse to multiple targets programmatically using sqlcmd, or using a tool like SQL Multi Script. You can use the /ScriptFile, /report, and /showWarnings flags to generate change scripts, difference reports and any warnings. See the commented out example in the PowerShell:
#$arguments = @("/scripts1:$($scriptsPath)", "/server2:$($_.serverName)", "/database2:$($_.databaseName)", "/ScriptFile:update_$($_.environment+"_"+$_.databaseName).sql", "/report:update_$($_.environment+"_"+$_.databaseName).html" , "/reportType:Interactive", "/showWarnings", "/include:Identical")
There is a drawback of running a pre-generated deployment script; it assumes that a given database target hasn’t drifted from its expected state. Often there are (rightly or wrongly) many individuals within an organization who have permissions to alter the production database, and changes can therefore be made outside of the prescribed development processes. The consequence is that at deployment time, the applied script has been validated against a target that no longer represents reality.
The solution here would be to add a check for drift prior to running the deployment script. This is achieved by using sqlcompare.exe to compare the target against the expected schema snapshot using the /Assertidentical flag. Should this return any differences (sqlcompare.exe Exit Code 79), a drift report is outputted instead of executing the deployment script. See the commented out example.
# $arguments = @("/scripts1:$($scriptsPath)", "/server2:$($_.serverName)", "/database2:$($_.databaseName)", "/Assertidentical")
Any checks and processes that should be undertaken prior to a manual deployment, should also be happen during an automated deployment. You might think about triggering backups prior to deployment – even better, automate the verification of the backup too.
You can use SQL Compare’s command line interface along with PowerShell to automate multiple actions and checks that you need in your deployment process.
Automation is a practical solution where multiple targets and a higher release cadence come into play. As we know, with great power comes great responsibility – responsibility to ensure that the necessary checks are made so deployments remain trouble-free.
(The code sample supplied in this post automates the simple dynamic deployment case – if you are considering more advanced automation, e.g. the drift checks, script generation, deploying to large numbers of targets and backup/verification, please email me at
[email protected] for further script samples or if you have further questions)