SqlPackageOnTargetMachines.ps1

function Import-SqlPs {
    push-location
    Import-Module SqlPS -ErrorAction 'SilentlyContinue' | out-null
    pop-location
}

function RunCommand
{
    param(
        [string]$command,
        [bool] $failOnErr = $true
    )

    $ErrorActionPreference = 'Continue'

    if( $psversiontable.PSVersion.Major -le 4)
    {
        $result = cmd.exe /c "`"$command`""  2>&1
    }
    else
    {

        Write-Verbose -Verbose $command
        $result = cmd.exe /c "$command"  2>&1
    }

    $ErrorActionPreference = 'Stop'

    if($failOnErr -and $LASTEXITCODE -ne 0)
    {
        throw $result
    }

    return $result
}

function Get-SqlPackageOnTargetMachine
{
    try
    {
        $sqlDacPath, $sqlVersion = LocateHighestVersionSqlPackageWithSql
        $sqlVersionNumber = [decimal] $sqlVersion
    }
    catch [System.Exception]
    {
        Write-Verbose ("Failed to get Dac Framework (installed with SQL Server) location with exception: " + $_.Exception.Message)
        $sqlVersionNumber = 0
    }

    try
    {
        $sqlMsiDacPath, $sqlMsiVersion = LocateHighestVersionSqlPackageWithDacMsi
        $sqlMsiVersionNumber = [decimal] $sqlMsiVersion
    }
    catch [System.Exception]
    {
        Write-Verbose ("Failed to get Dac Framework (installed with DAC Framework) location with exception: " + $_.Exception.Message)
        $sqlMsiVersionNumber = 0
    }

    try
    {
        $vsDacPath, $vsVersion = LocateHighestVersionSqlPackageInVS
        $vsVersionNumber = [decimal] $vsVersion
    }
    catch [System.Exception]
    {
        Write-Verbose ("Failed to get Dac Framework (installed with Visual Studio) location with exception: " + $_.Exception.Message)
        $vsVersionNumber = 0
    }

    $maximumVersion = [decimal]$(@($vsVersionNumber, $sqlVersionNumber, $sqlMsiVersionNumber) | Measure-Object -Maximum).Maximum 
    
    if ($sqlMsiVersionNumber -eq $maximumVersion)
    {
        $dacPath = $sqlMsiDacPath
    }
    elseif ($vsVersionNumber -eq $maximumVersion)
    {
        $dacPath = $vsDacPath
    }
    elseif ($sqlVersionNumber -eq $maximumVersion) 
    {
        $dacPath = $sqlDacPath
    }
    
    if ($dacPath -eq $null)
    {
        throw  "Unable to find the location of Dac Framework (SqlPackage.exe) from registry on machine $env:COMPUTERNAME"
    }
    else
    {
        return $dacPath
    }
}

function Get-RegistryValueIgnoreError
{
    param
    (
        [parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryHive]
        $RegistryHive,

        [parameter(Mandatory = $true)]
        [System.String]
        $Key,

        [parameter(Mandatory = $true)]
        [System.String]
        $Value,

        [parameter(Mandatory = $true)]
        [Microsoft.Win32.RegistryView]
        $RegistryView
    )

    try
    {
        $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView)
        $subKey =  $baseKey.OpenSubKey($Key)
        if($subKey -ne $null)
        {
            return $subKey.GetValue($Value)
        }
    }
    catch
    {
    }
    return $null
}

function Get-SubKeysInFloatFormat($keys)
{
    $targetKeys = @()
        foreach ($key in $keys)
        {
            try {
                $targetKeys += [decimal] $key
            }
            catch {}
        }

    $targetKeys
}

function Get-SqlPackageForSqlVersion([int] $majorVersion, [bool] $wow6432Node)
{
    $sqlInstallRootRegKey = "SOFTWARE", "Microsoft", "Microsoft SQL Server", "$majorVersion" -join [System.IO.Path]::DirectorySeparatorChar

    if ($wow6432Node -eq $true)
    {
        $sqlInstallRootPath = Get-RegistryValueIgnoreError LocalMachine "$sqlInstallRootRegKey" "VerSpecificRootDir" Registry64
    }
    else
    {
        $sqlInstallRootPath = Get-RegistryValueIgnoreError LocalMachine "$sqlInstallRootRegKey" "VerSpecificRootDir" Registry32
    }

    if ($sqlInstallRootPath -eq $null)
    {
        return $null
    }

    Write-Verbose "Sql Version Specific Root Dir for version $majorVersion as read from registry: $sqlInstallRootPath"

    $DacInstallPath = [System.IO.Path]::Combine($sqlInstallRootPath, "Dac", "bin", "SqlPackage.exe")

    if (Test-Path $DacInstallPath)
    {
        Write-Verbose "Dac Framework installed with SQL Version $majorVersion found at $DacInstallPath on machine $env:COMPUTERNAME"
        return $DacInstallPath
    }
    else
    {
        return $null
    }
}

function LocateHighestVersionSqlPackageWithSql()
{
    $sqlRegKey = "HKLM:", "SOFTWARE", "Wow6432Node", "Microsoft", "Microsoft SQL Server"-join [System.IO.Path]::DirectorySeparatorChar
    $sqlRegKey64 = "HKLM:", "SOFTWARE", "Microsoft", "Microsoft SQL Server"-join [System.IO.Path]::DirectorySeparatorChar

    if (-not (Test-Path $sqlRegKey))
    {
        $sqlRegKey = $sqlRegKey64
    }

    if (-not (Test-Path $sqlRegKey))
    {
        return $null, 0
    }

    $keys = Get-Item $sqlRegKey | %{$_.GetSubKeyNames()}
    $versions = Get-SubKeysInFloatFormat $keys | Sort-Object -Descending

    Write-Verbose "Sql Versions installed on machine $env:COMPUTERNAME as read from registry: $versions"

    foreach ($majorVersion in $versions)
    {
        $DacInstallPathWow6432Node = Get-SqlPackageForSqlVersion $majorVersion $true
        $DacInstallPath = Get-SqlPackageForSqlVersion $majorVersion $false

        if ($DacInstallPathWow6432Node -ne $null)
        {
            return $DacInstallPathWow6432Node, $majorVersion
        }
        elseif ($DacInstallPath -ne $null)
        {
            return $DacInstallPath, $majorVersion
        }
    }

    Write-Verbose "Dac Framework (installed with SQL) not found on machine $env:COMPUTERNAME"

    return $null, 0
}

function LocateHighestVersionSqlPackageWithDacMsi()
{
    $sqlDataTierFrameworkRegKeyWow = "HKLM:", "SOFTWARE", "Wow6432Node", "Microsoft", "Microsoft SQL Server", "Data-Tier Application Framework" -join [System.IO.Path]::DirectorySeparatorChar
    $sqlDataTierFrameworkRegKey = "HKLM:", "SOFTWARE", "Microsoft", "Microsoft SQL Server", "Data-Tier Application Framework" -join [System.IO.Path]::DirectorySeparatorChar

    if (-not (Test-Path $sqlDataTierFrameworkRegKey))
    {
        $sqlDataTierFrameworkRegKey = $sqlDataTierFrameworkRegKeyWow
    }

    if ((Test-Path $sqlDataTierFrameworkRegKey))
    {
        $keys = Get-Item $sqlDataTierFrameworkRegKey | %{$_.GetSubKeyNames()}
        $versions = Get-SubKeysInFloatFormat $keys | Sort-Object -Descending

        $installedMajorVersion = 0
        foreach ($majorVersion in $versions)
        {
            $sqlInstallRootRegKey = "SOFTWARE", "Microsoft", "Microsoft SQL Server", "Data-Tier Application Framework", "$majorVersion" -join [System.IO.Path]::DirectorySeparatorChar
            $sqlInstallRootPath64 = Get-RegistryValueIgnoreError LocalMachine "$sqlInstallRootRegKey" "InstallDir" Registry64
            $sqlInstallRootPath32 = Get-RegistryValueIgnoreError LocalMachine "$sqlInstallRootRegKey" "InstallDir" Registry32
            if ($sqlInstallRootPath64 -ne $null)
            {
                $sqlInstallRootPath = $sqlInstallRootPath64
                break
            }
            if ($sqlInstallRootPath32 -ne $null)
            {
                $sqlInstallRootPath = $sqlInstallRootPath32
                break
            }
        }

        $DacInstallPath = [System.IO.Path]::Combine($sqlInstallRootPath, "SqlPackage.exe")
        if (Test-Path $DacInstallPath)
        {
            Write-Verbose "Dac Framework installed with SQL Version $majorVersion found at $DacInstallPath on machine $env:COMPUTERNAME"
            return $DacInstallPath, $majorVersion
        }
    }

    $sqlRegKeyWow = "HKLM:", "SOFTWARE", "Wow6432Node", "Microsoft", "Microsoft SQL Server", "DACFramework", "CurrentVersion" -join [System.IO.Path]::DirectorySeparatorChar
    $sqlRegKey = "HKLM:", "SOFTWARE", "Microsoft", "Microsoft SQL Server", "DACFramework", "CurrentVersion" -join [System.IO.Path]::DirectorySeparatorChar

    $sqlKey = "SOFTWARE", "Microsoft", "Microsoft SQL Server", "DACFramework", "CurrentVersion" -join [System.IO.Path]::DirectorySeparatorChar

    if (Test-Path $sqlRegKey)
    {
        $dacVersion = Get-RegistryValueIgnoreError LocalMachine "$sqlKey" "Version" Registry64
        $majorVersion = $dacVersion.Substring(0, $dacVersion.IndexOf(".")) + "0"
    }

    if (Test-Path $sqlRegKeyWow)
    {
        $dacVersionX86 = Get-RegistryValueIgnoreError LocalMachine "$sqlKey" "Version" Registry32
        $majorVersionX86 = $dacVersionX86.Substring(0, $dacVersionX86.IndexOf(".")) + "0"
    }

    if ((-not($dacVersion)) -and (-not($dacVersionX86)))
    {
        Write-Verbose "Dac Framework (installed with DAC Framework) not found on machine $env:COMPUTERNAME"
        return $null, 0
    }

    if ($majorVersionX86 -gt $majorVersion)
    {
        $majorVersion = $majorVersionX86
    }

    $dacRelativePath = "Microsoft SQL Server", "$majorVersion", "DAC", "bin", "SqlPackage.exe" -join [System.IO.Path]::DirectorySeparatorChar
    $programFiles = $env:ProgramFiles
    $programFilesX86 = "${env:ProgramFiles(x86)}"

    if (-not ($programFilesX86 -eq $null))
    {
        $dacPath = $programFilesX86, $dacRelativePath -join [System.IO.Path]::DirectorySeparatorChar

        if (Test-Path("$dacPath"))
        {
            Write-Verbose "Dac Framework (installed with DAC Framework Msi) found on machine $env:COMPUTERNAME at $dacPath"
            return $dacPath, $majorVersion
        }
    }

    if (-not ($programFiles -eq $null))
    {
        $dacPath = $programFiles, $dacRelativePath -join [System.IO.Path]::DirectorySeparatorChar

        if (Test-Path($dacPath))
        {
            Write-Verbose "Dac Framework (installed with DAC Framework Msi) found on machine $env:COMPUTERNAME at $dacPath"
            return $dacPath, $majorVersion
        }
    }

    return $null, 0
}

function LocateSqlPackageFromVSInstallationRoot {
    [CmdletBinding()]
    Param (
        [string] $VSInstallRoot
    )
    Write-Verbose "Visual Studio install location: $VSInstallRoot"

    $sqlDacRoot = [System.IO.Path]::Combine($VSInstallRoot, "Extensions", "Microsoft", "SQLDB", "DAC")

    if (Test-Path $sqlDacRoot) {
        $sqlDacLocations = Get-ChildItem $sqlDacRoot | Sort-Object @{e={$_.Name -as [int]}} -Descending

        foreach ($sqlDacLocation in $sqlDacLocations)
        {
            $dacVersion = $sqlDacLocation.Name
            $dacFullPath = [System.IO.Path]::Combine($sqlDacLocation.FullName, "SqlPackage.exe")

            if(Test-Path $dacFullPath -pathtype leaf)
            {
                Write-Verbose "Dac Framework installed with Visual Studio found at $dacFullPath on machine $env:COMPUTERNAME"
                return $dacFullPath, $dacVersion
            }
            else
            {
                Write-Verbose "Unable to find Dac framework installed with Visual Studio at $($dacVersionDir.FullName) on machine $env:COMPUTERNAME"
            }
        }
    }
    return $null, 0
}

function LocateSqlPackageInVS([string] $version)
{
    $vsRegKeyForVersion = "SOFTWARE", "Microsoft", "VisualStudio", $version -join [System.IO.Path]::DirectorySeparatorChar

    $vsInstallDir = Get-RegistryValueIgnoreError LocalMachine "$vsRegKeyForVersion" "InstallDir" Registry64

    if ($vsInstallDir -eq $null)
    {
        $vsInstallDir = Get-RegistryValueIgnoreError LocalMachine "$vsRegKeyForVersion"  "InstallDir" Registry32
    }

    if ($vsInstallDir)
    {
        return (LocateSqlPackageFromVSInstallationRoot -VSInstallRoot $vsInstallDir)
    }

    return $null, 0
}

function Find-VSWhere {
    $vsWhereLocation = [System.IO.Path]::Combine(${env:ProgramFiles(x86)}, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')
    if (Test-Path $vsWhereLocation) {
        Write-Verbose "vswhere.exe location:'$vsWhereLocation'"
        return $vsWhereLocation
    }
    return $null
}

function LocateLatestVSVersionUsingVSWhere {
    [CmdletBinding()]
    Param ([string] $VSWherePath)
    Remove-Item variable:\LASTEXITCODE -ErrorAction 'SilentlyContinue'
    $vsInstallations = & $VSWherePath "-legacy" "-prerelease" "-format" "json"
    $vsInstallations = $($vsInstallations -join '').Trim()
    if ($LASTEXITCODE -ne 0) {
        # if lastexitcode is not 0, then vsinstallations variable will contain the error string
        throw "VSWhere exitcode: '$LASTEXITCODE', error: '$vsInstallations'"
    }
    if (![string]::IsNullOrEmpty($vsInstallations)) {
        $vsInstallations = ConvertFrom-Json $vsInstallations
        $maxVersion = [version]::new('0.0.0.0')
        $vsPath = ''
        foreach ($vsInstallation in $vsInstallations) {
            $version = [version]::new($vsInstallation.installationVersion)
            if ($version -gt $maxVersion) {
                $maxVersion = $version
                $vsPath = $vsInstallation.installationPath
            }
        }
        Write-Verbose "Latest Visual Studio (version: '$($maxVersion.ToString()))' found at: '$vsPath'"
        return $vsPath
    }
    Write-Verbose "Cannot locate any Visual Studio installation using vswhere.exe".
    return $null
}

function LocateHighestVersionSqlPackageInVS()
{
    $vsWherePath = Find-VSWhere
    if (![string]::IsNullOrEmpty($vsWherePath)) {
        try {
            $vsPath = LocateLatestVSVersionUsingVSWhere -VSWherePath $vsWherePath -ErrorAction 'Stop'
            if (![string]::IsNullOrEmpty($vsPath)) {
                $vsPath = [System.IO.Path]::Combine($vsPath, 'Common7', 'IDE')
                $dacFullPath, $dacVersion = LocateSqlPackageFromVSInstallationRoot -VSInstallRoot $vsPath
                if ($dacFullPath -ne $null) {
                    Write-Verbose "Detected sqlpackage.exe from Visual Studio installation using vswhere.exe. SqlPackage location: $dacFullPath, version: $dacVersion"
                    return $dacFullPath, $dacVersion
                }
            }
        } catch {
            Write-Verbose "Unable to locate sqlpackage.exe from Visual Studio installation using vswhere. Error: $($_.Exception.Message)"
        }
    }
    # fallback to detecting sqlpackage using the registry method if no vswhere is found or if an error was encountered
    $vsRegKey = "HKLM:", "SOFTWARE", "Wow6432Node", "Microsoft", "VisualStudio" -join [System.IO.Path]::DirectorySeparatorChar
    $vsRegKey64 = "HKLM:", "SOFTWARE", "Microsoft", "VisualStudio" -join [System.IO.Path]::DirectorySeparatorChar

    if (-not (Test-Path $vsRegKey))
    {
        $vsRegKey = $vsRegKey64
    }

    if (-not (Test-Path $vsRegKey))
    {
        Write-Verbose "Visual Studio not found on machine $env:COMPUTERNAME"
        return $null, 0
    }

    $keys = Get-Item $vsRegKey | %{$_.GetSubKeyNames()}
    $versions = Get-SubKeysInFloatFormat $keys | Sort-Object -Descending

    Write-Verbose "Visual Studio versions found on machine $env:COMPUTERNAME as read from registry: $versions"

    foreach ($majorVersion in $versions)
    {
        $dacFullPath, $dacVersion = LocateSqlPackageInVS $majorVersion

        if ($dacFullPath -ne $null)
        {
            return $dacFullPath, $dacVersion
        }
    }

    Write-Verbose "Dac Framework (installed with Visual Studio) not found on machine $env:COMPUTERNAME"

    return $null, 0
}

function Get-SqlPackageCmdArgs
{
    param (
    [string]$dacpacFile,
    [string]$targetMethod,
    [string]$serverName,
    [string]$databaseName,
    [string]$authscheme,
    [System.Management.Automation.PSCredential]$sqlServerCredentials,
    [string]$connectionString,
    [string]$publishProfile,
    [string]$additionalArguments
    )

    Write-Verbose -Verbose "File is $dacpacFile"

    # validate dacpac file
    if ([System.IO.Path]::GetExtension($dacpacFile) -ne ".dacpac")
    {
        throw "Invalid Dacpac file [ $dacpacFile ] provided"
    }

    $sqlPkgCmdArgs = [string]::Format(' /SourceFile:"{0}" /Action:Publish', $dacpacFile)

    if($targetMethod -eq "server")
    {
        $sqlPkgCmdArgs = [string]::Format('{0} /TargetServerName:"{1}"', $sqlPkgCmdArgs, $serverName)
        if ($databaseName)
        {
            $sqlPkgCmdArgs = [string]::Format('{0} /TargetDatabaseName:"{1}"', $sqlPkgCmdArgs, $databaseName)
        }

        if($authscheme -eq "sqlServerAuthentication")
        {
            if($sqlServerCredentials)
            {
                $sqlUsername = $sqlServerCredentials.UserName
                $sqlPassword = $sqlServerCredentials.GetNetworkCredential().password
                $sqlPkgCmdArgs = [string]::Format('{0} /TargetUser:"{1}" /TargetPassword:"{2}"', $sqlPkgCmdArgs, $sqlUsername, $sqlPassword)
            }
        }
    }
    elseif($targetMethod -eq "connectionString")
    {
        $sqlPkgCmdArgs = [string]::Format('{0} /TargetConnectionString:"{1}"', $sqlPkgCmdArgs, $connectionString)
    }

    if( ![string]::IsNullOrWhiteSpace($publishProfile) )
    {
        # validate publish profile
        if ([System.IO.Path]::GetExtension($publishProfile) -ne ".xml")
        {
            throw "Invalid Publish Profile [ $publishProfile ] provided"
        }
        $sqlPkgCmdArgs = [string]::Format('{0} /Profile:"{1}"', $sqlPkgCmdArgs, $publishProfile)
    }

    $sqlPkgCmdArgs = [string]::Format('{0} {1}', $sqlPkgCmdArgs, $additionalArguments)
    Write-Verbose "Sqlpackage.exe arguments : $sqlPkgCmdArgs"
    return $sqlPkgCmdArgs
}

function Invoke-DacpacDeployment
{
    param (
     [string]$dacpacFile,
     [string]$targetMethod,
     [string]$serverName,
     [string]$databaseName,
     [string]$authscheme,
     [System.Management.Automation.PSCredential]$sqlServerCredentials,
     [string]$connectionString,
     [string]$publishProfile,
     [string]$additionalArguments
    )
    
    Write-Verbose "Entering script SqlPackageOnTargetMachines.ps1"
    Import-SqlPs
    $sqlPackage = Get-SqlPackageOnTargetMachine
    $sqlPackageArguments = Get-SqlPackageCmdArgs -dacpacFile $dacpacFile -targetMethod $targetMethod -serverName $serverName -databaseName $databaseName -authscheme $authscheme -sqlServerCredentials $sqlServerCredentials -connectionString $connectionString -publishProfile $publishProfile -additionalArguments $additionalArguments
    if($databaseName -Split "" -Contains "*")
    {
        $databaseNamePattern = $databaseName -Replace "*", "%" -Replace "_", "[_]"
        $dbNames=Invoke-Sqlcmd -Query "SELECT name FROM sys.databases WHERE state=0 AND name LIKE '$dbNamePattern' ORDER BY name" -ConnectionString $connectionString
        foreach($dbName in $dbNames)
        {
            Write-Verbose "Found Database $($dbName)";
        }
    }
    $sqlInvokeCmd = Invoke-SqlCmd -ConnectionString $connectionString
    Write-Verbose -Verbose $sqlPackageArguments

    Write-Verbose "Executing command: $sqlPackage $sqlPackageArguments"
    ExecuteCommand -FileName "$sqlPackage"  -Arguments $sqlPackageArguments
}

function ExecuteCommand
{
    param(
        [String][Parameter(Mandatory=$true)] $FileName,
        [String][Parameter(Mandatory=$true)] $Arguments
    )

    if( $psversiontable.PSVersion.Major -lt 4)
    {
        $ErrorActionPreference = 'Continue'
        $command = "`"$FileName`" $Arguments"
        $result = cmd.exe /c "`"$command`""
    }
    else
    {
        $ErrorActionPreference = 'SilentlyContinue'
        $result = ""
        Invoke-Expression "& '$FileName' --% $Arguments"  -ErrorVariable errors | ForEach-Object {
            $result +=  ("$_ " + [Environment]::NewLine)
        }

        foreach($errorMsg in $errors){
            $result +=  "$errorMsg "
        }
    }

    $ErrorActionPreference = 'Stop'
    if($LASTEXITCODE -ne 0)
    {
         Write-Verbose "Deployment failed with error : $result"
         throw  $result
    }

    return $result
}