This was used successfully on Visual Studio 2015 Professional on a Windows 10 laptop to deploy changes to SQL Azure V12.
I hope it will work for you as well. :)
First create the Migrations.sql
table and the fnMigrationExists.sql
helper functions and stored procedures.
Then edit your project properties, go to Build Events
and paste the Pre-Build event command line into the window.
This windows batch script will check the migrations folders and dynamically build Pre and Post migration files for you automatically.
Create a Migrations
folder in your DB Project.
In the Migrations Folder, Create a Post
folder, a Pre
folder, as well as Script.PostDeployment.sql
and Script.PreDeployment.sql
.
Make sure BuildAction is set to PostDeploy and PreDeploy respectively.
Now you just have to place your migrations as .sql files in the Pre and Post folders and the build script will automagically pull the sql files from the folders in and create Pre and Post deployment scripts for you.
Your folder structure should look like this:
Project
├── Functions/
│ ├── fnMigrationExists.sql
├── Migrations/
│ ├── Post/
│ │ ├── 001 - My First Post Migration.sql
│ ├── Pre/
│ │ ├── 001 - My First Pre Migration.sql
│ ├── Script.PostDeployment.sql
│ ├── Script.PreDeployment.sql
├── Security/
├── Stored Procedures/
├── Tables/
│ ├── Migrations.sql
├── Views/
Important: Make sure you add your migrations as Build Action: None and Do not copy!
Here is a small example of the log when you deploy the project with migrations:
Skipped: Post-Deployment Migration Post\007 - Remove LegacyTable1 data and table.sql - already applied
Skipped: Post-Deployment Migration Post\008 - Remove LegacyTable2 data and table.sql - already applied
Executing: Post-Deployment Migration Post\009 - Remove SettingHistoryType 4 Status.sql
HistoryTypes has entry with Id 4 and Name Status - removing
HistoryTypes entry for Status removed.
Success: Post-Deployment Migration Post\009 - Remove SettingHistoryType 4 Status.sql applied
Executing: Post-Deployment Migration Post\010 - Remove unused Substatus 10 Save.sql
Status has entry with Id 10 and Name Save - removing
Status entry for Save removed.
Success: Post-Deployment Migration Post\010 - Remove unused Substatus 10 Save.sql applied
Here is small example of the generated Script.PreDeployment.sql
:
--Generated By Pre-build event command line tool
DECLARE @migration_status int = dbo.fnMigrationExists('Pre\105 - ZipCodes column nullability changes.sql', '00 c2 f2 47 68 1b 8d 82 30 b4 b1 7b 50 9a 25 82');
if(@migration_status = 0)
BEGIN
SET XACT_ABORT ON
begin transaction
PRINT N'Executing: Pre-Deployment Migration Pre\105 - ZipCodes column nullability changes.sql';
:r ".\Pre\105 - ZipCodes column nullability changes.sql"
INSERT INTO [dbo].[Migrations] ([Filename], [Hash]) VALUES ('Pre\105 - ZipCodes column nullability changes.sql', '00 c2 f2 47 68 1b 8d 82 30 b4 b1 7b 50 9a 25 82')
commit transaction
PRINT N'Success: Pre-Deployment Migration Pre\105 - ZipCodes column nullability changes.sql applied';
END
if(@migration_status = 1)
BEGIN
PRINT N'Skipped: Pre-Deployment Migration Pre\105 - ZipCodes column nullability changes.sql - already applied'
END
if(@migration_status = 2)
BEGIN
PRINT N'Error: Pre-Deployment Migration Pre\105 - ZipCodes column nullability changes.sql hash mismatch'
RAISERROR(50001, 10, 127)
END
GO
DECLARE @migration_status int = dbo.fnMigrationExists('Pre\106 - Remove Carriers LineHaulTruckCost column.sql', 'e5 c3 8d 90 92 10 82 a0 07 3b 08 72 d2 a1 6b 0d');
if(@migration_status = 0)
BEGIN
SET XACT_ABORT ON
begin transaction
PRINT N'Executing: Pre-Deployment Migration Pre\106 - Remove Carriers LineHaulTruckCost column.sql';
:r ".\Pre\106 - Remove Carriers LineHaulTruckCost column.sql"
INSERT INTO [dbo].[Migrations] ([Filename], [Hash]) VALUES ('Pre\106 - Remove Carriers LineHaulTruckCost column.sql', 'e5 c3 8d 90 92 10 82 a0 07 3b 08 72 d2 a1 6b 0d')
commit transaction
PRINT N'Success: Pre-Deployment Migration Pre\106 - Remove Carriers LineHaulTruckCost column.sql applied';
END
if(@migration_status = 1)
BEGIN
PRINT N'Skipped: Pre-Deployment Migration Pre\106 - Remove Carriers LineHaulTruckCost column.sql - already applied'
END
if(@migration_status = 2)
BEGIN
PRINT N'Error: Pre-Deployment Migration Pre\106 - Remove Carriers LineHaulTruckCost column.sql hash mismatch'
RAISERROR(50001, 10, 127)
END
GO
Finally, as part of this project I had to re-evaluate our nullable columns. I attached a linqpad script (Nullability.cs
).
It produces output like this:
// TableName ColumnName ColumnString DefaultValue NullCount DefaultCount NotNullOrDefaultCount
// CityRecord Elevation INT 0 71796 35635 2096528
// CityRecord FeatureClass CHAR '' 2704 0 2205745
// Coupon MaxUses INT 0 3 1 5
// User UserName NVARCHAR(256) '' 1 1 500
The counts are there to help you make a informed decision on whether nullability is used.
The DefaultValue column is meant to be manually adjusted to whatever default you need.
I then copy the first 4 columns ( TableName, ColumnName, ColumnString and DefaultValue ) into my Sublime Text and run RegEx on it. Here is a visual representation of the regex I use: https://regex101.com/r/cD9tV1/3
Input:
CityRecord Elevation INT 0
CityRecord FeatureClass CHAR ''
Coupon MaxUses INT 0
User UserName NVARCHAR(256) ''
Search:
^(?'Table'\w*)\s+(?'Column'\w*)\s+(?'Type'\w*(?:\((?:\w*|\d*\, \d*)\))?)\s+(?'Default'[\w\'\? \X\(\)\.]*?)$
Replace:
NOT NULL CONSTRAINT [DF_$+{Table}_$+{Column}] DEFAULT (($+{Default}))
If dbo.fnTableExists('$+{Table}') = 1 AND dbo.fnColumnExists('$+{Table}', '$+{Column}') = 1
Begin
DECLARE @$+{Table}_$+{Column}_def $+{Type} = $+{Default};
PRINT N'$+{Table} exists and $+{Column} column exists. Defaulting data to '+TRY_CAST(@$+{Table}_$+{Column}_def AS NVARCHAR(MAX))+'.';
exec [dbo].[sp_DropIndicesOnColumn] '$+{Table}', '$+{Column}';
UPDATE [dbo].[$+{Table}] SET [$+{Column}] = @$+{Table}_$+{Column}_def WHERE [$+{Column}] IS NULL;
ALTER TABLE [dbo].[$+{Table}] ALTER COLUMN [$+{Column}] $+{Type} NOT NULL;
PRINT N'$+{Column} column has been defaulted to '+TRY_CAST(@$+{Table}_$+{Column}_def AS NVARCHAR(MAX))+' and set to not null.';
End
Result:
NOT NULL CONSTRAINT [DF_CityRecord_Elevation] DEFAULT ((0))
If dbo.fnTableExists('CityRecord') = 1 AND dbo.fnColumnExists('CityRecord', 'Elevation') = 1
Begin
DECLARE @CityRecord_Elevation_def INT = 0;
PRINT N'CityRecord exists and Elevation column exists. Defaulting data to '+TRY_CAST(@CityRecord_Elevation_def AS NVARCHAR(MAX))+'.';
exec [dbo].[sp_DropIndicesOnColumn] 'CityRecord', 'Elevation';
UPDATE [dbo].[CityRecord] SET [Elevation] = @CityRecord_Elevation_def WHERE [Elevation] IS NULL;
ALTER TABLE [dbo].[CityRecord] ALTER COLUMN [Elevation] INT NOT NULL;
PRINT N'Elevation column has been defaulted to '+TRY_CAST(@CityRecord_Elevation_def AS NVARCHAR(MAX))+' and set to not null.';
End
NOT NULL CONSTRAINT [DF_CityRecord_FeatureClass] DEFAULT ((''))
If dbo.fnTableExists('CityRecord') = 1 AND dbo.fnColumnExists('CityRecord', 'FeatureClass') = 1
Begin
DECLARE @CityRecord_FeatureClass_def CHAR = '';
PRINT N'CityRecord exists and FeatureClass column exists. Defaulting data to '+TRY_CAST(@CityRecord_FeatureClass_def AS NVARCHAR(MAX))+'.';
exec [dbo].[sp_DropIndicesOnColumn] 'CityRecord', 'FeatureClass';
UPDATE [dbo].[CityRecord] SET [FeatureClass] = @CityRecord_FeatureClass_def WHERE [FeatureClass] IS NULL;
ALTER TABLE [dbo].[CityRecord] ALTER COLUMN [FeatureClass] CHAR NOT NULL;
PRINT N'FeatureClass column has been defaulted to '+TRY_CAST(@CityRecord_FeatureClass_def AS NVARCHAR(MAX))+' and set to not null.';
End
NOT NULL CONSTRAINT [DF_Coupon_MaxUses] DEFAULT ((0))
If dbo.fnTableExists('Coupon') = 1 AND dbo.fnColumnExists('Coupon', 'MaxUses') = 1
Begin
DECLARE @Coupon_MaxUses_def INT = 0;
PRINT N'Coupon exists and MaxUses column exists. Defaulting data to '+TRY_CAST(@Coupon_MaxUses_def AS NVARCHAR(MAX))+'.';
exec [dbo].[sp_DropIndicesOnColumn] 'Coupon', 'MaxUses';
UPDATE [dbo].[Coupon] SET [MaxUses] = @Coupon_MaxUses_def WHERE [MaxUses] IS NULL;
ALTER TABLE [dbo].[Coupon] ALTER COLUMN [MaxUses] INT NOT NULL;
PRINT N'MaxUses column has been defaulted to '+TRY_CAST(@Coupon_MaxUses_def AS NVARCHAR(MAX))+' and set to not null.';
End
NOT NULL CONSTRAINT [DF_User_UserName] DEFAULT ((''))
If dbo.fnTableExists('User') = 1 AND dbo.fnColumnExists('User', 'UserName') = 1
Begin
DECLARE @User_UserName_def NVARCHAR(256) = '';
PRINT N'User exists and UserName column exists. Defaulting data to '+TRY_CAST(@User_UserName_def AS NVARCHAR(MAX))+'.';
exec [dbo].[sp_DropIndicesOnColumn] 'User', 'UserName';
UPDATE [dbo].[User] SET [UserName] = @User_UserName_def WHERE [UserName] IS NULL;
ALTER TABLE [dbo].[User] ALTER COLUMN [UserName] NVARCHAR(256) NOT NULL;
PRINT N'UserName column has been defaulted to '+TRY_CAST(@User_UserName_def AS NVARCHAR(MAX))+' and set to not null.';
End
I cut out the first line over each block and use that to replace the NULL
in the table definition.
The rest goes into a migration file.
Hope that helps!