Skip to content

Commit

Permalink
Merge pull request #2366 from kmadof/add_support_for_publish_profiles
Browse files Browse the repository at this point in the history
Add support for publish profiles
  • Loading branch information
matthid authored Dec 17, 2019
2 parents 1aac550 + 513052a commit 990cc69
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 1 deletion.
5 changes: 5 additions & 0 deletions help/markdown/sql-dacpac.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<p>This documentation is for FAKE version 5.0 or later. The old documentation can be found <a href="legacy-dacpac.html">here</a></p>
</div>

<div class="alert alert-info">
<h5>INFO</h5>
<p>This module is deprecated. Please use <a href="sql-sqlpackage.html">SqlPackage</a> module instead.</p>
</div>

FAKE can be used to create a SQL DACPAC and also deploy it to a SQL Server using the MSDeploy executable. This is installed by default with Visual Studio and with the SQL Server Data Tools (SSDT) package.

DACPACs automatically diff from the source to the destination and generate the SQL script dynamically.
Expand Down
54 changes: 54 additions & 0 deletions help/markdown/sql-sqlpackage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Packaging and Deploying SQL Databases

<div class="alert alert-info">
<h5>INFO</h5>
<p>This documentation is for FAKE version 5.0 or later. The old documentation can be found <a href="legacy-dacpac.html">here</a></p>
</div>

FAKE can be used to create a SQL DACPAC and also deploy it to a SQL Server using the MSDeploy executable. This is installed by default with Visual Studio and with the SQL Server Data Tools (SSDT) package.

DACPACs automatically diff from the source to the destination and generate the SQL script dynamically.

You can read up more on DACPac and MSDeploy arguments [here](https://msdn.microsoft.com/en-us/library/hh550081%28v=vs.103%29.aspx).

## Sample usage

Ensure that you have already built your database project (you can do this with standard MSBuild). Then, use the ``deployDb`` command to deploy the ``dbProject.dacpac`` to the ``myDatabase``.

open Fake.Sql

/// the database for local development + compile
Target.create "DeployLocalDb" (fun _ ->
let connectionString = "Data Source=(localdb)\\MSSQLLocalDB;Integrated Security=True;Database=MyDatabase;Pooling=False"
let dacPacPath = "path/to/dbProject.dacpac"
SqlPackage.deployDb (fun args -> { args with Source = dacPacPath; Destination = connectionString }) |> ignore
)

## Deployment Options

You can optionally specify the type of command to use (again, refer to the documentation above for more detail): -

* Deploy - full deploy to destination
* Script - SQL script
* Report - XML report of diff

In addition, you can also elect to deploy to Dacpac files rather than SQL databases - simply pass in the pass to the dacpac that you wish to generate.


## Arguments

You can provide following arguments (in brackets are given sqlpackage.exe parameters name):

* SqlPackageToolPath - path to sqlpackage.exe
* Action - deployment option (/a)
* Source - specifies a source file to be used as the source of action instead of a database (/SourceFile)
* Destination - specifies a valid SQL Server/Azure connection string to the target database (/TargetConnectionString)
* Timeout - specifies the command timeout in seconds when executing queries against SQL Server (/p:CommandTimeout)
* BlockOnPossibleDataLoss - Specifies that the publish episode should be terminated if there is a possibility of data loss resulting from the publish.operation (/p:BlockOnPossibleDataLoss)
* DropObjectsNotInSource - specifies whether objects that do not exist in the database snapshot (.dacpac) file will be dropped from the target database when you publish to a database (/p:DropObjectsNotInSource)
* RecreateDb - specifies whether the target database should be updated or whether it should be dropped and re-created when you publish to a database (/p:CreateNewDatabase)
* AdditionalSqlPackageProperties - specifies a name value pair for an properties;{PropertyName}={Value}
* Variables - specifies a name value pair for an action-specific variable;{VariableName}={Value}. The DACPAC file contains the list of valid SQLCMD variables (/v)
* Profile - specifies the file path to a DAC Publish Profile (/pr)

If both DAC Publish Profile file and command line parameters provides the same argument, then the one from command line overrites Publish Profile value. An example: if Profile File has BlockOnPossibleDataLoss set to true and command line set it to false, sqlpackage.exe set BlockOnPossibleDataLoss to false.
1 change: 1 addition & 0 deletions src/app/Fake.Sql.DacPac/Fake.Sql.DacPac.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Sql.DacPac.fs" />
<Compile Include="Sql.SqlPackage.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Fake.Core.Environment\Fake.Core.Environment.fsproj" />
Expand Down
4 changes: 3 additions & 1 deletion src/app/Fake.Sql.DacPac/Sql.DacPac.fs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
namespace Fake.Sql

open System

/// Contains helpers around deploying databases.
[<RequireQualifiedAccess>]
[<Obsolete("Use SqlPackage instead")>]
module DacPac =

open Fake.Core
open Fake.IO.FileSystemOperators
open Fake.IO.Globbing.Operators
open System.IO
open System

/// The type of action to execute.
type DeployAction =
Expand Down
195 changes: 195 additions & 0 deletions src/app/Fake.Sql.DacPac/Sql.SqlPackage.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
namespace Fake.Sql

/// Contains helpers around deploying databases.
[<RequireQualifiedAccess>]
module SqlPackage =

open Fake.Core
open Fake.IO.FileSystemOperators
open Fake.IO.Globbing.Operators
open System.IO
open System

/// The type of action to execute.
type DeployAction =
/// Generate and apply a synchronisation script between two databases.
| Deploy
/// Generate a SQL script to sync two databases.
| Script of OutputPath:string
/// Generate an XML report for the differences between two databases.
| Report of OutputPath:string

/// Configuration arguments for DacPac deploy
type DeployDbArgs = {
/// The path to SqlPackage.exe.
SqlPackageToolPath : string
/// Type of action to execute. Defaults to Deploy.
Action : DeployAction
/// Path to source (path to DACPAC or Connection String).
Source : string
/// Path to destination (path to DACPAC or Connection String).
Destination : string
/// Timeout for deploy (defaults to 120 seconds).
Timeout : int option
/// Block deployment if data loss can occur. Defaults to true.
BlockOnPossibleDataLoss : bool option
/// Drops objects in the destination that do not exist in the source. Defaults to false.
DropObjectsNotInSource : bool option
/// Recreates the database from scratch on publish (rather than an in-place update). Defaults to false.
RecreateDb : bool option
/// Additional configuration parameters required by sqlpackage.exe
AdditionalSqlPackageProperties : (string * string) list
/// SQLCMD variables
Variables : (string * string) list
///Specifies the file path to a DAC Publish Profile. The profile defines a collection of properties and variables to use when generating outputs.
Profile : string }

let validPaths =
let getSqlVersion (path:string) = path.Split '\\' |> Array.item 3 |> int
let getVsVersion path = (Path.GetDirectoryName path |> DirectoryInfo).Name |> int
let sql = !!(Environment.ProgramFilesX86 </> @"Microsoft SQL Server\**\DAC\bin\SqlPackage.exe") |> Seq.map(fun path -> path, getSqlVersion path)
let vs = !!(Environment.ProgramFilesX86 </> @"Microsoft Visual Studio*\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\*\SqlPackage.exe") |> Seq.map(fun path -> path, getVsVersion path)
let vs2017 = !!(Environment.ProgramFilesX86 </> @"Microsoft Visual Studio\**\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\*\SqlPackage.exe") |> Seq.map(fun path -> path, getVsVersion path)

[ sql; vs; vs2017 ]
|> List.collect Seq.toList
|> List.sortByDescending snd
|> List.map fst

/// The default DacPac deployment arguments.
let DefaultDeploymentArgs =
{ SqlPackageToolPath =
validPaths
|> List.tryHead
|> defaultArg <| ""
Action = Deploy
Source = ""
Destination = ""
Timeout = None
BlockOnPossibleDataLoss = None
DropObjectsNotInSource = None
RecreateDb = None
AdditionalSqlPackageProperties = []
Variables = []
Profile = "" }

[<Literal>]
let Action = "Action"

[<Literal>]
let Source = "Source"

[<Literal>]
let Destination = "Destination"

[<Literal>]
let OutputPath = "OutputPath"

[<Literal>]
let BlockOnPossibleDataLoss = "BlockOnPossibleDataLoss"

[<Literal>]
let DropObjectsNotInSource = "DropObjectsNotInSource"

[<Literal>]
let Timeout = "Timeout"

[<Literal>]
let RecreateDb = "RecreateDb"

[<Literal>]
let AdditionalSqlPackageProperties = "AdditionalSqlPackageProperties"

[<Literal>]
let Variables = "Variables"

[<Literal>]
let Profile = "Profile"

let private formatArgument (args:DeployDbArgs) action outputPath additionalParameters variables argumentName =

match argumentName with
| Action -> sprintf "/Action:%s" action
| Source -> sprintf """/SourceFile:"%s" """ args.Source
| Destination when not(String.isNullOrEmpty(args.Destination)) -> sprintf """/TargetConnectionString:"%s" """ args.Destination
| OutputPath -> sprintf "%s" outputPath
| BlockOnPossibleDataLoss when args.BlockOnPossibleDataLoss.IsSome -> sprintf "/p:BlockOnPossibleDataLoss=%b" args.BlockOnPossibleDataLoss.Value
| BlockOnPossibleDataLoss when String.isNullOrEmpty(args.Profile) && args.BlockOnPossibleDataLoss.IsNone -> sprintf "/p:BlockOnPossibleDataLoss=%b" false
| DropObjectsNotInSource when args.DropObjectsNotInSource.IsSome -> sprintf "/p:DropObjectsNotInSource=%b" args.DropObjectsNotInSource.Value
| DropObjectsNotInSource when String.isNullOrEmpty(args.Profile) && args.DropObjectsNotInSource.IsNone -> sprintf "/p:DropObjectsNotInSource=%b" false
| Timeout when args.Timeout.IsSome -> sprintf "/p:CommandTimeout=%d" args.Timeout.Value
| Timeout when String.isNullOrEmpty(args.Profile) && args.Timeout.IsNone -> sprintf "/p:CommandTimeout=%d" args.Timeout.Value
| RecreateDb when args.RecreateDb.IsSome -> sprintf "/p:CreateNewDatabase=%b" args.RecreateDb.Value
| RecreateDb when String.isNullOrEmpty(args.Profile) && args.RecreateDb.IsNone -> sprintf "/p:CreateNewDatabase=%b" false
| AdditionalSqlPackageProperties when not(String.isNullOrEmpty(additionalParameters)) -> sprintf "%s" additionalParameters
| Variables when not(String.isNullOrEmpty(variables)) -> sprintf "%s" variables
| Profile when not(System.String.IsNullOrEmpty(args.Profile)) -> sprintf "/pr:%s" args.Profile
| _ -> ""

module PropertyKeys =
/// When creating a new SQL Azure database, specifies the database service tier to use e.g. S2, P1
let sqlAzureDbSize = "DatabaseServiceObjective"

let private generateCommandLine args =
let action, outputPath =
match args with
| Deploy -> "Publish", None
| Script outputPath -> "Script", Some outputPath
| Report outputPath -> "DeployReport", Some outputPath
let outputPath = defaultArg(outputPath |> Option.map(sprintf """/OutputPath:"%s" """)) ""
action, outputPath

let private formatDacPacArguments args action outputPath additionalParameters variables =

let format = formatArgument args action outputPath additionalParameters variables

let actionParameter = format Action
let sourceParameter = format Source
let destinationParameter = format Destination
let outputPathParameter = format OutputPath
let blockOnPossibleDataLossParameter = format BlockOnPossibleDataLoss
let dropObjectsNotInSourceParameter = format DropObjectsNotInSource
let timeoutParameter = format Timeout
let recreateDbParameter = format RecreateDb
let additionalSqlPackagePropertiesParameter = format AdditionalSqlPackageProperties
let variablesParameter = format Variables
let profileParameter = format Profile

[ actionParameter; sourceParameter; destinationParameter; outputPathParameter; blockOnPossibleDataLossParameter; dropObjectsNotInSourceParameter; timeoutParameter; recreateDbParameter; additionalSqlPackagePropertiesParameter; variablesParameter; profileParameter ]
|> List.filter (fun item -> item <> "")
|> String.concat " "

/// Deploys a SQL DacPac or database to another database or DacPac.
let deployDb setParams =
let args = setParams DefaultDeploymentArgs
let action, outputPath = generateCommandLine args.Action

let concat parameter =
List.map (fun (key, value) -> sprintf "/%s:%s=%s" parameter key value)
>> String.concat " "

let additionalParameters = args.AdditionalSqlPackageProperties |> concat "p"

let variables = args.Variables |> concat "v"

let arguments = formatDacPacArguments args action outputPath additionalParameters variables

if System.String.IsNullOrWhiteSpace args.SqlPackageToolPath then
failwith "No SqlPackage.exe filename was given."

if not (File.Exists args.SqlPackageToolPath) then
let paths =
if validPaths |> List.contains args.SqlPackageToolPath then validPaths
else [ args.SqlPackageToolPath ]
failwithf "Unable to find a valid instance of SqlPackage.exe. Paths checked were: %A." paths

CreateProcess.fromRawCommandLine args.SqlPackageToolPath arguments
|> CreateProcess.withTimeout TimeSpan.MaxValue
|> CreateProcess.addOnExited (fun data exitCode ->
if exitCode <> 0 then
failwithf "Process exit code '%d' <> 0." exitCode
else
data)
|> Proc.run
|> ignore

0 comments on commit 990cc69

Please sign in to comment.