├── .gitignore ├── LICENSE ├── README.md ├── build.cake ├── build.ps1 ├── build.sh ├── buildconfig.cake ├── docker-compose.yml ├── init-local-config.sh ├── resources ├── .env.sample ├── integrationtests.local.ini.sample └── nuget-usage.md ├── src ├── Directory.Build.props ├── DotNet-SqlDb.sln ├── projects │ └── SqlDb │ │ ├── Commands │ │ ├── Command.fs │ │ ├── DropDbCommand.fs │ │ ├── EnsureDbCommand.fs │ │ └── UpgradeDbCommand.fs │ │ ├── Common.fs │ │ ├── DbUp.fs │ │ ├── DbUpScript.fs │ │ ├── Errors.fs │ │ ├── Logging.fs │ │ ├── MsSql.fs │ │ ├── Program.fs │ │ └── SqlDb.fsproj └── tests │ ├── IntegrationTests │ ├── Config.fs │ ├── DropDbCommandTests.fs │ ├── EnsureDbCommandTests.fs │ ├── IntegrationTests.fsproj │ ├── Should.fs │ ├── SqlScript │ │ └── 001_CreateTables.sql │ ├── TestEnv.fs │ └── UpgradeDbCommandTests.fs │ └── UnitTests │ ├── CommonTests.fs │ ├── DbUpScriptTests.fs │ ├── ErrorsTests.fs │ ├── LoggingTests.fs │ ├── Should.fs │ ├── SqlDbTests.fs │ └── UnitTests.fsproj └── tools └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | # vs 2 | .vs/ 3 | [Aa]pp_[Dd]ata/ 4 | [Bb]in/ 5 | [D]ebug/ 6 | [Aa]rtifacts/ 7 | [Oo]bj/ 8 | [Tt]est[Rr]esult/ 9 | [Tt]est[Rr]esults/ 10 | [Tt]emp/ 11 | *.user 12 | *.suo 13 | *.cache 14 | 15 | .env 16 | *.local 17 | *.local.json 18 | *.local.ini 19 | 20 | # deploy 21 | build/ 22 | tools/** 23 | !tools/packages.config 24 | 25 | # misc 26 | *~ 27 | *.swp 28 | *.sdf 29 | *.orig 30 | *.pfx 31 | 32 | # resharper 33 | _ReSharper.* 34 | *.resharper* 35 | *.[Rr]e[Ss]harper.user 36 | *.DotSettings.user 37 | 38 | #windows stuff 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Wertheim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-sqldb 2 | Uses [DbUp](https://github.com/dbup/dbup) and [Command Line Parser](https://github.com/commandlineparser/commandline) to offer a simple [DotNet Global Tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) for applying migration scripts etc. against a SQL-Server database. 3 | 4 | [![Build status](https://dev.azure.com/danielwertheim/dotnet-sqldb/_apis/build/status/dotnet-sqldb-CI-Ubuntu)](https://dev.azure.com/danielwertheim/dotnet-sqldb/_build/latest) 5 | [![NuGet](https://img.shields.io/nuget/v/dotnet-sqldb.svg)](http://nuget.org/packages/dotnet-sqldb) 6 | 7 | **Note:** It's your data. Use at your own risk. As [the license](https://github.com/danielwertheim/dotnet-sqldb/blob/master/LICENSE) (MIT) says: *"THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND..."* 8 | 9 | ## Installation 10 | It's a DotNet Global Tool, distributed via NuGet. 11 | 12 | ### Global installation 13 | To install it globally (machine), you use the [dotnet tool install command](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install) with the `-g` switch: 14 | 15 | ``` 16 | dotnet tool install -g dotnet-sqldb 17 | ``` 18 | 19 | After that you can start using it (you might need to restart your prompt of choice): 20 | 21 | ``` 22 | dotnet sqldb --help 23 | ``` 24 | 25 | or 26 | 27 | ``` 28 | dotnet-sqldb --help 29 | ``` 30 | 31 | ### Local installation 32 | To install it locally, you use the [dotnet tool install command](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install) with the `--tool-path` switch: 33 | 34 | ``` 35 | dotnet tool install dotnet-sqldb --tool-path /path/for/tool 36 | ``` 37 | 38 | To use it you will need to include it in the current environment path. 39 | 40 | ## Commands 41 | The following commands are supported: 42 | 43 | - **Ensure:** Ensures that the specified DB exists. If it does not exist, it gets created. 44 | ``` 45 | dotnet sqldb ensure [--connectionstring|-c]=mycnstring 46 | ``` 47 | 48 | or 49 | 50 | ``` 51 | dotnet-sqldb ensure [--connectionstring|-c]=mycnstring 52 | ``` 53 | 54 | - **Drop:** Drops the specified database if it exists 55 | ``` 56 | dotnet sqldb drop [--connectionstring|-c]=mycnstring 57 | ``` 58 | 59 | or 60 | 61 | ``` 62 | dotnet-sqldb drop [--connectionstring|-c]=mycnstring 63 | ``` 64 | 65 | - **Up:** Upgrades the database by applying SQL-scripts using [DbUp](https://github.com/dbup/dbup) 66 | ``` 67 | dotnet sqldb up [--connectionstring|-c]=mycnstring [--assembly|-a]=myassembly_with_embedded_scripts 68 | ``` 69 | 70 | or 71 | 72 | ``` 73 | dotnet-sqldb up [--connectionstring|-c]=mycnstring [--assembly|-a]=myassembly_with_embedded_scripts 74 | ``` 75 | 76 | ## Integration tests 77 | The `./.env` file and `./src/tests/IntegrationTests/integrationtests.local.ini` files are `.gitignored`. In order to create sample files of these, you can run: 78 | 79 | ``` 80 | . init-local-config.sh 81 | ``` 82 | 83 | ### Docker-Compose 84 | There's a `docker-compose.yml` file, that defines usage of an SQL Server instance over port `1401`. The `SA_PASSWORD` is configured via environment key `DNSQLDB_SA_PWD`, which can either be specified via: 85 | 86 | - Environment variable: `DNSQLDB_SA_PWD`, e.g.: 87 | ``` 88 | DNSQLDB_SA_PWD=MyFooPassword 89 | ``` 90 | 91 | - Docker Environment file `./.env` (`.gitignored`), e.g.: 92 | ``` 93 | DNSQLDB_SA_PWD=MyFooPassword 94 | ``` 95 | 96 | ### Test configuration 97 | A connection string needs to be provided, either via: 98 | 99 | - Local-INI-file (`.gitignored`): `./src/tests/IntegrationTests/integrationtests.local.ini` containing a connection string section with a `TestDb` node, e.g.: 100 | ``` 101 | [ConnectionStrings] 102 | TestDb="Server=.,1401;Database=foodb;User ID=sa;Password=MyFooPassword" 103 | ``` 104 | 105 | - Environment variable: `DNSQLDB_ConnectionStrings__TestDb`, e.g.: 106 | 107 | ``` 108 | DNSQLDB_ConnectionStrings__TestDb=Server=.,1401;Database=foodb;User ID=sa;Password=MyFooPassword 109 | ``` 110 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | #load "./buildconfig.cake" 2 | 3 | var config = BuildConfig.Create(Context, BuildSystem); 4 | var verbosity = DotNetCoreVerbosity.Minimal; 5 | 6 | Information($"SrcDir: '{config.SrcDir}'"); 7 | Information($"ArtifactsDir: '{config.ArtifactsDir}'"); 8 | Information($"SemVer: '{config.SemVer}'"); 9 | Information($"BuildVersion: '{config.BuildVersion}'"); 10 | Information($"BuildProfile: '{config.BuildProfile}'"); 11 | 12 | Task("Default") 13 | .IsDependentOn("Clean") 14 | .IsDependentOn("Build") 15 | .IsDependentOn("UnitTests") 16 | .IsDependentOn("IntegrationTests"); 17 | 18 | Task("CI") 19 | .IsDependentOn("Docker-Compose-Up") 20 | .IsDependentOn("Default") 21 | .IsDependentOn("Pack") 22 | .Finally(() => { 23 | Information("Running 'docker-compose down'..."); 24 | StartProcess("docker-compose", "down"); 25 | }); 26 | /********************************************/ 27 | Task("Clean").Does(() => { 28 | EnsureDirectoryExists(config.ArtifactsDir); 29 | CleanDirectory(config.ArtifactsDir); 30 | }); 31 | 32 | Task("Build").Does(() => { 33 | var settings = new DotNetCoreBuildSettings { 34 | Configuration = config.BuildProfile, 35 | NoIncremental = true, 36 | NoRestore = false, 37 | Verbosity = verbosity, 38 | MSBuildSettings = new DotNetCoreMSBuildSettings() 39 | .WithProperty("TreatWarningsAsErrors", "true") 40 | .WithProperty("Version", config.SemVer) 41 | .WithProperty("AssemblyVersion", config.BuildVersion) 42 | .WithProperty("FileVersion", config.BuildVersion) 43 | .WithProperty("InformationalVersion", config.BuildVersion) 44 | }; 45 | 46 | foreach(var sln in GetFiles($"{config.SrcDir}*.sln")) { 47 | DotNetCoreBuild(sln.FullPath, settings); 48 | } 49 | }); 50 | 51 | Task("UnitTests").Does(() => { 52 | var settings = new DotNetCoreTestSettings { 53 | Configuration = config.BuildProfile, 54 | NoBuild = true, 55 | NoRestore = true, 56 | Logger = "trx", 57 | ResultsDirectory = config.TestResultsDir, 58 | Verbosity = verbosity 59 | }; 60 | foreach(var testProj in GetFiles($"{config.SrcDir}tests/**/UnitTests.fsproj")) { 61 | DotNetCoreTest(testProj.FullPath, settings); 62 | } 63 | }); 64 | 65 | Task("Docker-Compose-Up").Does(() => { 66 | StartProcess("docker-compose", "up -d"); 67 | }); 68 | 69 | Task("IntegrationTests").Does(() => { 70 | var settings = new DotNetCoreTestSettings { 71 | Configuration = config.BuildProfile, 72 | NoBuild = true, 73 | NoRestore = true, 74 | Logger = "trx", 75 | ResultsDirectory = config.TestResultsDir, 76 | Verbosity = verbosity 77 | }; 78 | foreach(var testProj in GetFiles($"{config.SrcDir}tests/**/IntegrationTests.fsproj")) { 79 | DotNetCoreTest(testProj.FullPath, settings); 80 | } 81 | }); 82 | 83 | Task("Pack").Does(() => { 84 | var settings = new DotNetCorePackSettings 85 | { 86 | Configuration = config.BuildProfile, 87 | OutputDirectory = config.ArtifactsDir, 88 | NoRestore = true, 89 | NoBuild = true, 90 | Verbosity = verbosity, 91 | MSBuildSettings = new DotNetCoreMSBuildSettings() 92 | .WithProperty("Version", config.SemVer) 93 | .WithProperty("AssemblyVersion", config.BuildVersion) 94 | .WithProperty("FileVersion", config.BuildVersion) 95 | .WithProperty("InformationalVersion", config.BuildVersion) 96 | }; 97 | 98 | foreach(var proj in GetFiles($"{config.SrcDir}projects/**/*.fsproj")) { 99 | DotNetCorePack(proj.FullPath, settings); 100 | } 101 | }); 102 | 103 | RunTarget(config.Target); -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER ShowDescription 25 | Shows description about tasks. 26 | .PARAMETER DryRun 27 | Performs a dry run. 28 | .PARAMETER Experimental 29 | Uses the nightly builds of the Roslyn script engine. 30 | .PARAMETER Mono 31 | Uses the Mono Compiler rather than the Roslyn script engine. 32 | .PARAMETER SkipToolPackageRestore 33 | Skips restoring of packages. 34 | .PARAMETER ScriptArgs 35 | Remaining arguments are added here. 36 | 37 | .LINK 38 | https://cakebuild.net 39 | 40 | #> 41 | 42 | [CmdletBinding()] 43 | Param( 44 | [string]$Script = "build.cake", 45 | [string]$Target, 46 | [string]$Configuration, 47 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 48 | [string]$Verbosity, 49 | [switch]$ShowDescription, 50 | [Alias("WhatIf", "Noop")] 51 | [switch]$DryRun, 52 | [switch]$Experimental, 53 | [switch]$Mono, 54 | [switch]$SkipToolPackageRestore, 55 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 56 | [string[]]$ScriptArgs 57 | ) 58 | 59 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 60 | function MD5HashFile([string] $filePath) 61 | { 62 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 63 | { 64 | return $null 65 | } 66 | 67 | [System.IO.Stream] $file = $null; 68 | [System.Security.Cryptography.MD5] $md5 = $null; 69 | try 70 | { 71 | $md5 = [System.Security.Cryptography.MD5]::Create() 72 | $file = [System.IO.File]::OpenRead($filePath) 73 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 74 | } 75 | finally 76 | { 77 | if ($file -ne $null) 78 | { 79 | $file.Dispose() 80 | } 81 | } 82 | } 83 | 84 | function GetProxyEnabledWebClient 85 | { 86 | $wc = New-Object System.Net.WebClient 87 | $proxy = [System.Net.WebRequest]::GetSystemWebProxy() 88 | $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials 89 | $wc.Proxy = $proxy 90 | return $wc 91 | } 92 | 93 | Write-Host "Preparing to run build script..." 94 | 95 | if(!$PSScriptRoot){ 96 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 97 | } 98 | 99 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 100 | $ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" 101 | $MODULES_DIR = Join-Path $TOOLS_DIR "Modules" 102 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 103 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 104 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 105 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 106 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 107 | $ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" 108 | $MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" 109 | 110 | # Make sure tools folder exists 111 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 112 | Write-Verbose -Message "Creating tools directory..." 113 | New-Item -Path $TOOLS_DIR -Type directory | out-null 114 | } 115 | 116 | # Make sure that packages.config exist. 117 | if (!(Test-Path $PACKAGES_CONFIG)) { 118 | Write-Verbose -Message "Downloading packages.config..." 119 | try { 120 | $wc = GetProxyEnabledWebClient 121 | $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { 122 | Throw "Could not download packages.config." 123 | } 124 | } 125 | 126 | # Try find NuGet.exe in path if not exists 127 | if (!(Test-Path $NUGET_EXE)) { 128 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 129 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } 130 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 131 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 132 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 133 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 134 | } 135 | } 136 | 137 | # Try download NuGet.exe if not exists 138 | if (!(Test-Path $NUGET_EXE)) { 139 | Write-Verbose -Message "Downloading NuGet.exe..." 140 | try { 141 | $wc = GetProxyEnabledWebClient 142 | $wc.DownloadFile($NUGET_URL, $NUGET_EXE) 143 | } catch { 144 | Throw "Could not download NuGet.exe." 145 | } 146 | } 147 | 148 | # Save nuget.exe path to environment to be available to child processed 149 | $ENV:NUGET_EXE = $NUGET_EXE 150 | 151 | # Restore tools from NuGet? 152 | if(-Not $SkipToolPackageRestore.IsPresent) { 153 | Push-Location 154 | Set-Location $TOOLS_DIR 155 | 156 | # Check for changes in packages.config and remove installed tools if true. 157 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 158 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 159 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 160 | Write-Verbose -Message "Missing or changed package.config hash..." 161 | Remove-Item * -Recurse -Exclude packages.config,nuget.exe 162 | } 163 | 164 | Write-Verbose -Message "Restoring tools from NuGet..." 165 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 166 | 167 | if ($LASTEXITCODE -ne 0) { 168 | Throw "An error occurred while restoring NuGet tools." 169 | } 170 | else 171 | { 172 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 173 | } 174 | Write-Verbose -Message ($NuGetOutput | out-string) 175 | 176 | Pop-Location 177 | } 178 | 179 | # Restore addins from NuGet 180 | if (Test-Path $ADDINS_PACKAGES_CONFIG) { 181 | Push-Location 182 | Set-Location $ADDINS_DIR 183 | 184 | Write-Verbose -Message "Restoring addins from NuGet..." 185 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" 186 | 187 | if ($LASTEXITCODE -ne 0) { 188 | Throw "An error occurred while restoring NuGet addins." 189 | } 190 | 191 | Write-Verbose -Message ($NuGetOutput | out-string) 192 | 193 | Pop-Location 194 | } 195 | 196 | # Restore modules from NuGet 197 | if (Test-Path $MODULES_PACKAGES_CONFIG) { 198 | Push-Location 199 | Set-Location $MODULES_DIR 200 | 201 | Write-Verbose -Message "Restoring modules from NuGet..." 202 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" 203 | 204 | if ($LASTEXITCODE -ne 0) { 205 | Throw "An error occurred while restoring NuGet modules." 206 | } 207 | 208 | Write-Verbose -Message ($NuGetOutput | out-string) 209 | 210 | Pop-Location 211 | } 212 | 213 | # Make sure that Cake has been installed. 214 | if (!(Test-Path $CAKE_EXE)) { 215 | Throw "Could not find Cake.exe at $CAKE_EXE" 216 | } 217 | 218 | 219 | 220 | # Build Cake arguments 221 | $cakeArguments = @("$Script"); 222 | if ($Target) { $cakeArguments += "-target=$Target" } 223 | if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } 224 | if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } 225 | if ($ShowDescription) { $cakeArguments += "-showdescription" } 226 | if ($DryRun) { $cakeArguments += "-dryrun" } 227 | if ($Experimental) { $cakeArguments += "-experimental" } 228 | if ($Mono) { $cakeArguments += "-mono" } 229 | $cakeArguments += $ScriptArgs 230 | 231 | # Start Cake 232 | Write-Host "Running build script..." 233 | &$CAKE_EXE $cakeArguments 234 | exit $LASTEXITCODE -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ########################################################################## 4 | # This is the Cake bootstrapper script for Linux and OS X. 5 | # This file was downloaded from https://github.com/cake-build/resources 6 | # Feel free to change this file to fit your needs. 7 | ########################################################################## 8 | 9 | # Define directories. 10 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 11 | TOOLS_DIR=$SCRIPT_DIR/tools 12 | ADDINS_DIR=$TOOLS_DIR/Addins 13 | MODULES_DIR=$TOOLS_DIR/Modules 14 | NUGET_EXE=$TOOLS_DIR/nuget.exe 15 | CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe 16 | PACKAGES_CONFIG=$TOOLS_DIR/packages.config 17 | PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum 18 | ADDINS_PACKAGES_CONFIG=$ADDINS_DIR/packages.config 19 | MODULES_PACKAGES_CONFIG=$MODULES_DIR/packages.config 20 | 21 | # Define md5sum or md5 depending on Linux/OSX 22 | MD5_EXE= 23 | if [[ "$(uname -s)" == "Darwin" ]]; then 24 | MD5_EXE="md5 -r" 25 | else 26 | MD5_EXE="md5sum" 27 | fi 28 | 29 | # Define default arguments. 30 | SCRIPT="build.cake" 31 | CAKE_ARGUMENTS=() 32 | 33 | # Parse arguments. 34 | for i in "$@"; do 35 | case $1 in 36 | -s|--script) SCRIPT="$2"; shift ;; 37 | --) shift; CAKE_ARGUMENTS+=("$@"); break ;; 38 | *) CAKE_ARGUMENTS+=("$1") ;; 39 | esac 40 | shift 41 | done 42 | 43 | # Make sure the tools folder exist. 44 | if [ ! -d "$TOOLS_DIR" ]; then 45 | mkdir "$TOOLS_DIR" 46 | fi 47 | 48 | # Make sure that packages.config exist. 49 | if [ ! -f "$TOOLS_DIR/packages.config" ]; then 50 | echo "Downloading packages.config..." 51 | curl -Lsfo "$TOOLS_DIR/packages.config" https://cakebuild.net/download/bootstrapper/packages 52 | if [ $? -ne 0 ]; then 53 | echo "An error occurred while downloading packages.config." 54 | exit 1 55 | fi 56 | fi 57 | 58 | # Download NuGet if it does not exist. 59 | if [ ! -f "$NUGET_EXE" ]; then 60 | echo "Downloading NuGet..." 61 | curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe 62 | if [ $? -ne 0 ]; then 63 | echo "An error occurred while downloading nuget.exe." 64 | exit 1 65 | fi 66 | fi 67 | 68 | # Restore tools from NuGet. 69 | pushd "$TOOLS_DIR" >/dev/null 70 | if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then 71 | find . -type d ! -name . ! -name 'Cake.Bakery' | xargs rm -rf 72 | fi 73 | 74 | mono "$NUGET_EXE" install -ExcludeVersion 75 | if [ $? -ne 0 ]; then 76 | echo "Could not restore NuGet tools." 77 | exit 1 78 | fi 79 | 80 | $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" 81 | 82 | popd >/dev/null 83 | 84 | # Restore addins from NuGet. 85 | if [ -f "$ADDINS_PACKAGES_CONFIG" ]; then 86 | pushd "$ADDINS_DIR" >/dev/null 87 | 88 | mono "$NUGET_EXE" install -ExcludeVersion 89 | if [ $? -ne 0 ]; then 90 | echo "Could not restore NuGet addins." 91 | exit 1 92 | fi 93 | 94 | popd >/dev/null 95 | fi 96 | 97 | # Restore modules from NuGet. 98 | if [ -f "$MODULES_PACKAGES_CONFIG" ]; then 99 | pushd "$MODULES_DIR" >/dev/null 100 | 101 | mono "$NUGET_EXE" install -ExcludeVersion 102 | if [ $? -ne 0 ]; then 103 | echo "Could not restore NuGet modules." 104 | exit 1 105 | fi 106 | 107 | popd >/dev/null 108 | fi 109 | 110 | # Make sure that Cake has been installed. 111 | if [ ! -f "$CAKE_EXE" ]; then 112 | echo "Could not find Cake.exe at '$CAKE_EXE'." 113 | exit 1 114 | fi 115 | 116 | # Start Cake 117 | exec mono "$CAKE_EXE" $SCRIPT "${CAKE_ARGUMENTS[@]}" -------------------------------------------------------------------------------- /buildconfig.cake: -------------------------------------------------------------------------------- 1 | public class BuildConfig 2 | { 3 | private const string Version = "0.1.4"; 4 | private const bool IsPreRelease = false; 5 | 6 | public readonly string SrcDir = "./src/"; 7 | public readonly string ArtifactsDir = "./artifacts/"; 8 | public readonly string TestResultsDir = "./testresults/"; 9 | 10 | public string Target { get; private set; } 11 | public string SemVer { get; private set; } 12 | public string BuildVersion { get; private set; } 13 | public string BuildProfile { get; private set; } 14 | 15 | public static BuildConfig Create( 16 | ICakeContext context, 17 | BuildSystem buildSystem) 18 | { 19 | if (context == null) 20 | throw new ArgumentNullException("context"); 21 | 22 | var buildRevision = context.Argument("buildrevision", "0"); 23 | 24 | return new BuildConfig 25 | { 26 | Target = context.Argument("target", "Default"), 27 | SemVer = Version + (IsPreRelease ? $"-pre{buildRevision}" : string.Empty), 28 | BuildVersion = Version + "." + buildRevision, 29 | BuildProfile = context.Argument("configuration", "Release") 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | sqldb: 4 | image: "mcr.microsoft.com/mssql/server:2017-latest" 5 | environment: 6 | ACCEPT_EULA: "Y" 7 | MSSQL_PID: "Express" 8 | SA_PASSWORD: ${DNSQLDB_SA_PWD} 9 | ports: 10 | - "1401:1433" -------------------------------------------------------------------------------- /init-local-config.sh: -------------------------------------------------------------------------------- 1 | cp ./resources/.env.sample .env 2 | cp ./resources/integrationtests.local.ini.sample ./src/tests/integrationtests/integrationtests.local.ini -------------------------------------------------------------------------------- /resources/.env.sample: -------------------------------------------------------------------------------- 1 | DNSQLDB_SA_PWD=MyFooPassword 2 | -------------------------------------------------------------------------------- /resources/integrationtests.local.ini.sample: -------------------------------------------------------------------------------- 1 | [ConnectionStrings] 2 | TestDb="Server=.,1401;Database=foodb;User ID=sa;Password=MyFooPassword" -------------------------------------------------------------------------------- /resources/nuget-usage.md: -------------------------------------------------------------------------------- 1 | ## Global installation 2 | ``` 3 | dotnet tool install -g dotnet-sqldb 4 | ``` 5 | 6 | Usage either: `dotnet-sqldb` or `dotnet sqldb` 7 | 8 | ## Help 9 | 10 | ``` 11 | dotnet-sqldb --help 12 | ``` 13 | 14 | ## EnsureDb 15 | Ensures that the specified DB exists. If it does not exist, it gets created. 16 | 17 | ``` 18 | dotnet-sqldb ensure [--connectionstring|-c] mycnstring 19 | ``` 20 | 21 | ## DropDb 22 | Drops the specified database if it exists. 23 | 24 | ``` 25 | dotnet-sqldb drop [--connectionstring|-c] mycnstring 26 | ``` 27 | 28 | ## UpgradeDb 29 | Upgrades the database by applying SQL-scripts using [DbUp](https://github.com/dbup/dbup) 30 | 31 | ``` 32 | dotnet-sqldb up [--connectionstring|-c] mycnstring [--assembly|-a] myassembly_with_embedded_scripts 33 | ``` 34 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.0.0 4 | .NET Global Tool using e.g. DbUp for SqlServer to manage migration scripts. 5 | danielwertheim 6 | danielwertheim 7 | Copyright danielwertheim 2019 8 | https://github.com/danielwertheim/dotnet-sqldb 9 | Git 10 | latest 11 | 12 | -------------------------------------------------------------------------------- /src/DotNet-SqlDb.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.329 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8C4A4BD2-46A9-4C30-9D07-ADC15B0F35DA}" 7 | ProjectSection(SolutionItems) = preProject 8 | Directory.Build.props = Directory.Build.props 9 | EndProjectSection 10 | EndProject 11 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SqlDb", "projects\SqlDb\SqlDb.fsproj", "{40C771FE-915C-474A-B39C-557748C8EA52}" 12 | EndProject 13 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "UnitTests", "tests\UnitTests\UnitTests.fsproj", "{3AD116DB-C951-495E-8F57-9D7CBD30CD66}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8C81C048-225C-4647-B173-3BEF807242D2}" 16 | EndProject 17 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.fsproj", "{CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {40C771FE-915C-474A-B39C-557748C8EA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {40C771FE-915C-474A-B39C-557748C8EA52}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {40C771FE-915C-474A-B39C-557748C8EA52}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {40C771FE-915C-474A-B39C-557748C8EA52}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {3AD116DB-C951-495E-8F57-9D7CBD30CD66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {3AD116DB-C951-495E-8F57-9D7CBD30CD66}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {3AD116DB-C951-495E-8F57-9D7CBD30CD66}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {3AD116DB-C951-495E-8F57-9D7CBD30CD66}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(NestedProjects) = preSolution 42 | {3AD116DB-C951-495E-8F57-9D7CBD30CD66} = {8C81C048-225C-4647-B173-3BEF807242D2} 43 | {CF41BFF6-FEC0-4CFD-8588-C6FE99D549C8} = {8C81C048-225C-4647-B173-3BEF807242D2} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {E383C75D-C7F0-4753-B3C4-6D61FD1F7F96} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/projects/SqlDb/Commands/Command.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Commands.Command 2 | 3 | open SqlDb 4 | open Serilog.Events 5 | 6 | let inline tryRun<'opts when 'opts : (member Verbose : bool)> commandName (f: 'opts -> Result) (opts: 'opts) = 7 | try 8 | let verbose = ((^opts) : (member Verbose : bool) (opts)) 9 | 10 | if verbose = true then Logging.overrideLogLevelWith LogEventLevel.Verbose 11 | 12 | f opts 13 | with ex -> 14 | Errors.CommandError (commandName, ex) |> Error -------------------------------------------------------------------------------- /src/projects/SqlDb/Commands/DropDbCommand.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Commands.DbDropCommand 2 | 3 | open CommandLine 4 | open DbUp 5 | open SqlDb 6 | open SqlDb.Logging 7 | 8 | let name = CommandName "DropDbCommand" 9 | 10 | let private logger = Logging.loggerFor LoggingContexts.DbDropCommand 11 | 12 | [] 13 | type Options = { 14 | [] 17 | ConnectionString : string 18 | 19 | [] 23 | Verbose: bool 24 | } 25 | 26 | let run (opts:Options) = 27 | logger.Debug("Running with Options={@DbDropOptions}", opts); 28 | 29 | opts.ConnectionString 30 | |> MsSql.ConnectionInfo.fromConnectionString 31 | |> fun cnInfo -> 32 | DeployChanges.To.SqlDatabase(cnInfo.MasterConnectionString |> MsSql.MasterConnectionString.asString) 33 | |> DbUp'.useScript (DbUpScript.dropDb (MsSql.DbConnectionString.getDbName cnInfo.DbConnectionString)) 34 | |> DbUp'.useSerilog logger 35 | |> DbUp'.useNullJournal 36 | |> DbUp'.build 37 | |> DbUp'.run 38 | -------------------------------------------------------------------------------- /src/projects/SqlDb/Commands/EnsureDbCommand.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Commands.DbEnsureCommand 2 | 3 | open CommandLine 4 | open DbUp 5 | open SqlDb 6 | open SqlDb.Logging 7 | 8 | let name = CommandName "EnsureDbCommand" 9 | 10 | let private logger = Logging.loggerFor LoggingContexts.DbEnsureCommand 11 | 12 | [] 13 | type Options = { 14 | [] 17 | ConnectionString : string 18 | 19 | [] 23 | Verbose: bool 24 | } 25 | 26 | let run (opts:Options) = 27 | logger.Debug("Running with Options={@DbEnsureOptions}", opts); 28 | 29 | opts.ConnectionString 30 | |> MsSql.ConnectionInfo.fromConnectionString 31 | |> fun cnInfo -> 32 | DeployChanges.To.SqlDatabase(cnInfo.MasterConnectionString |> MsSql.MasterConnectionString.asString) 33 | |> DbUp'.useScript (DbUpScript.ensureDbExists (MsSql.DbConnectionString.getDbName cnInfo.DbConnectionString)) 34 | |> DbUp'.useSerilog logger 35 | |> DbUp'.useNullJournal 36 | |> DbUp'.build 37 | |> DbUp'.run -------------------------------------------------------------------------------- /src/projects/SqlDb/Commands/UpgradeDbCommand.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Commands.DbUpCommand 2 | 3 | open System.Reflection 4 | open CommandLine 5 | open DbUp 6 | open SqlDb 7 | open SqlDb.Logging 8 | 9 | let name = CommandName "UpgradeDbCommand" 10 | 11 | let private logger = Logging.loggerFor LoggingContexts.DbUpCommand 12 | 13 | [] 14 | type Options = { 15 | [] 18 | ConnectionString : string 19 | 20 | [] 23 | Assembly : string 24 | 25 | [] 29 | Verbose: bool 30 | } 31 | 32 | let run (opts:Options) = 33 | logger.Debug("Running with Options={@DbUpOptions}", opts); 34 | 35 | opts.ConnectionString 36 | |> MsSql.ConnectionInfo.fromConnectionString 37 | |> fun cnInfo -> 38 | DeployChanges.To.SqlDatabase(cnInfo.DbConnectionString |> MsSql.DbConnectionString.asString) 39 | |> DbUp'.useScriptsInAssembly (Assembly.LoadFrom(opts.Assembly)) 40 | |> DbUp'.useSerilog logger 41 | |> DbUp'.logScriptOutput 42 | |> DbUp'.build 43 | |> DbUp'.run -------------------------------------------------------------------------------- /src/projects/SqlDb/Common.fs: -------------------------------------------------------------------------------- 1 | namespace SqlDb 2 | 3 | type CommandName = CommandName of string 4 | 5 | module CommandName = 6 | let asString (CommandName cn) = cn -------------------------------------------------------------------------------- /src/projects/SqlDb/DbUp.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.DbUp' 2 | 3 | open DbUp.Engine.Output 4 | open DbUp.Builder 5 | open DbUp.Engine 6 | open DbUp.Helpers 7 | open Serilog 8 | 9 | type private DbUpLogger(logger:ILogger) = 10 | interface IUpgradeLog with 11 | member __.WriteInformation (format:string, []args:obj[]) = 12 | logger.Information(format, args) 13 | member __.WriteWarning (format:string, []args:obj[]) = 14 | logger.Warning(format, args) 15 | member __.WriteError (format:string, []args:obj[]) = 16 | logger.Error(format, args) 17 | 18 | let useSerilog (logger:ILogger) (builder: UpgradeEngineBuilder) = 19 | builder.LogTo (new DbUpLogger(logger)) 20 | 21 | let useNullJournal (builder:UpgradeEngineBuilder) = 22 | builder.JournalTo(new NullJournal()) 23 | 24 | let useScript ((name, script):DbUpScript) (builder: UpgradeEngineBuilder) = 25 | builder.WithScript(name, script) 26 | 27 | let useScriptsInAssembly assembly (builder: UpgradeEngineBuilder) = 28 | builder.WithScriptsEmbeddedInAssembly(assembly) 29 | 30 | let logScriptOutput (builder: UpgradeEngineBuilder) = 31 | builder.LogScriptOutput() 32 | 33 | let run (engine: UpgradeEngine) : Result<_, Errors> = 34 | let result = engine.PerformUpgrade() 35 | 36 | match result.Successful with 37 | | true -> Ok() 38 | | _ -> Errors.DbUpEngineError ( 39 | result.Error.Message |> ErrorMessage, 40 | result.Error) |> Error 41 | 42 | let build (builder:UpgradeEngineBuilder) = 43 | builder.Build() -------------------------------------------------------------------------------- /src/projects/SqlDb/DbUpScript.fs: -------------------------------------------------------------------------------- 1 | namespace SqlDb 2 | 3 | open System 4 | open System.Data 5 | open DbUp.Engine 6 | 7 | type private DbDropScript(dbName:MsSql.DbName) = 8 | interface IScript with 9 | member __.ProvideScript (commandFactory:Func) = 10 | let dbNameString = dbName |> MsSql.DbName.asString 11 | 12 | use command = commandFactory.Invoke() 13 | command.CommandType <- CommandType.Text 14 | command.CommandText <- sprintf """ 15 | IF DB_ID(N'%s') IS NOT NULL BEGIN 16 | ALTER DATABASE [%s] SET SINGLE_USER WITH ROLLBACK IMMEDIATE 17 | DROP DATABASE IF EXISTS [%s] 18 | END""" dbNameString dbNameString dbNameString 19 | command.ExecuteNonQuery() 20 | |> ignore 21 | 22 | String.Empty 23 | 24 | type private DbEnsureScript(dbName:MsSql.DbName) = 25 | interface IScript with 26 | member __.ProvideScript (commandFactory:Func) = 27 | let dbNameString = dbName |> MsSql.DbName.asString 28 | use command = commandFactory.Invoke() 29 | command.CommandType <- CommandType.Text 30 | command.CommandText <- sprintf "IF DB_ID (N'%s') IS NULL CREATE DATABASE [%s]" dbNameString dbNameString 31 | command.ExecuteNonQuery() 32 | |> ignore 33 | 34 | String.Empty 35 | 36 | type DbUpScript = string * IScript 37 | 38 | module DbUpScript = 39 | 40 | let dropDb dbName : DbUpScript = "DbDropScript", new DbDropScript(dbName) :> IScript 41 | 42 | let ensureDbExists dbName : DbUpScript = "DbEnsureScript", new DbEnsureScript(dbName) :> IScript -------------------------------------------------------------------------------- /src/projects/SqlDb/Errors.fs: -------------------------------------------------------------------------------- 1 | namespace SqlDb 2 | 3 | type ErrorMessage = ErrorMessage of string 4 | 5 | module ErrorMessage = 6 | let asString (ErrorMessage msg) = msg 7 | 8 | type Errors = 9 | | GenericError of ErrorMessage 10 | | DbUpEngineError of ErrorMessage*exn 11 | | CommandError of CommandName*exn -------------------------------------------------------------------------------- /src/projects/SqlDb/Logging.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Logging 2 | 3 | open Serilog 4 | open Serilog.Core 5 | open Serilog.Events 6 | 7 | type LoggingContexts = 8 | | DbDropCommand 9 | | DbEnsureCommand 10 | | DbUpCommand 11 | 12 | module LoggingContext = 13 | let asString (ctx: LoggingContexts) = 14 | ctx.ToString() 15 | 16 | let private logLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Information) 17 | 18 | let overrideLogLevelWith minimumLevel = 19 | logLevelSwitch.MinimumLevel <- minimumLevel 20 | 21 | let logger = 22 | Log.Logger <- (new LoggerConfiguration()) 23 | .MinimumLevel.ControlledBy(logLevelSwitch) 24 | .WriteTo.Console(outputTemplate="{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}") 25 | .Enrich.FromLogContext() 26 | .CreateLogger() :> ILogger 27 | 28 | Log.Logger 29 | 30 | let loggerFor (context:LoggingContexts) = 31 | logger.ForContext(Constants.SourceContextPropertyName, LoggingContext.asString context) 32 | 33 | -------------------------------------------------------------------------------- /src/projects/SqlDb/MsSql.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module SqlDb.MsSql 3 | 4 | open System 5 | open System.Data.SqlClient 6 | 7 | type DbName = private DbName of string 8 | type DbConnectionString = private DbConnectionString of DbName*string 9 | type MasterConnectionString = private MasterConnectionString of string 10 | 11 | type ConnectionInfo = { 12 | DbConnectionString: DbConnectionString 13 | MasterConnectionString: MasterConnectionString 14 | } 15 | 16 | module DbName = 17 | let private isMasterDb name = 18 | String.Equals (name, "master", StringComparison.OrdinalIgnoreCase) 19 | 20 | let create (dbName: string) = 21 | if (dbName |> isMasterDb) then 22 | invalidArg "dbName" "Invalid db name specified. Can not be against master db." 23 | 24 | DbName dbName 25 | 26 | let asString (DbName v) = v 27 | 28 | module DbConnectionString = 29 | let create connectionString = 30 | let builder = new SqlConnectionStringBuilder(connectionString) 31 | 32 | let dbName = DbName.create builder.InitialCatalog 33 | 34 | DbConnectionString (dbName,builder.ToString()) 35 | 36 | let getDbName (DbConnectionString (dbName, _)) = dbName 37 | 38 | let asString (DbConnectionString (_, cnString)) = cnString 39 | 40 | module MasterConnectionString = 41 | let create connectionString = 42 | let builder = new SqlConnectionStringBuilder(connectionString) 43 | 44 | builder.InitialCatalog <- "master" 45 | 46 | MasterConnectionString <| builder.ToString() 47 | 48 | let asString (MasterConnectionString v) = v 49 | 50 | module ConnectionInfo = 51 | 52 | let fromConnectionString connectionString = { 53 | DbConnectionString = connectionString |> DbConnectionString.create 54 | MasterConnectionString = connectionString |> MasterConnectionString.create 55 | } -------------------------------------------------------------------------------- /src/projects/SqlDb/Program.fs: -------------------------------------------------------------------------------- 1 | module SqlDb.Program 2 | 3 | open CommandLine 4 | open SqlDb.Logging 5 | open SqlDb.Commands 6 | open Serilog.Events 7 | 8 | type ExitCode = 9 | | Success = 0 10 | | Failure = -1 11 | 12 | [] 13 | let main argv = 14 | #if DEBUG 15 | Logging.overrideLogLevelWith LogEventLevel.Debug 16 | #endif 17 | 18 | argv 19 | |> Parser.Default.ParseArguments 20 | |> function 21 | | :? CommandLine.Parsed as command -> 22 | match command.Value with 23 | | :? DbDropCommand.Options as opts -> Command.tryRun DbDropCommand.name DbDropCommand.run opts 24 | | :? DbEnsureCommand.Options as opts -> Command.tryRun DbEnsureCommand.name DbEnsureCommand.run opts 25 | | :? DbUpCommand.Options as opts -> Command.tryRun DbUpCommand.name DbUpCommand.run opts 26 | | opts -> 27 | logger.Error("Parsed UnknownOptions={@UnknownOptions}", opts) 28 | Errors.GenericError (ErrorMessage "Parser error") |> Result.Error 29 | | :? CommandLine.NotParsed -> Result.Ok () 30 | | _ -> failwith "Parsing ended up in unhandled case." 31 | |> function 32 | | Result.Ok _ -> ExitCode.Success 33 | | Result.Error err -> 34 | match err with 35 | | Errors.GenericError msg -> logger.Error(msg |> ErrorMessage.asString) 36 | | Errors.DbUpEngineError (msg, ex) -> logger.Error(ex, msg |> ErrorMessage.asString) 37 | | Errors.CommandError (name, ex) -> logger.Error(ex, sprintf "Error while running command '%s'." (name |> CommandName.asString)) 38 | |> fun () -> ExitCode.Failure 39 | |> fun exitCode -> 40 | Serilog.Log.CloseAndFlush() 41 | int exitCode -------------------------------------------------------------------------------- /src/projects/SqlDb/SqlDb.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | true 6 | dotnet-sqldb 7 | netcoreapp2.2 8 | false 9 | true 10 | dotnet-sqldb 11 | https://github.com/danielwertheim/dotnet-sqldb 12 | MIT 13 | dotnet tool sqlserver sql migration 14 | 15 | 16 | 17 | DEBUG 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 | -------------------------------------------------------------------------------- /src/tests/IntegrationTests/Config.fs: -------------------------------------------------------------------------------- 1 | module Config 2 | 3 | open Microsoft.Extensions.Configuration 4 | 5 | let private readConfig () = 6 | let builder = new ConfigurationBuilder() 7 | builder 8 | .AddIniFile("integrationtests.local.ini", optional=true, reloadOnChange=false) 9 | .AddEnvironmentVariables("DNSQLDB_") 10 | .Build() 11 | 12 | let private config = readConfig() 13 | 14 | let getTestDbConnectionString () = 15 | config.GetConnectionString "TestDb" -------------------------------------------------------------------------------- /src/tests/IntegrationTests/DropDbCommandTests.fs: -------------------------------------------------------------------------------- 1 | module ``Drop Db command`` 2 | 3 | open Xunit 4 | 5 | module ``When db does not exist`` = 6 | open TestEnv 7 | open SqlDb 8 | 9 | [] 10 | let ``It should not fail`` () = 11 | use session = TestDbSession.beginNew() 12 | 13 | TestDb.Should.notExist session |> ignore 14 | 15 | SqlDb.Program.main [| 16 | "drop" 17 | "-c" 18 | MsSql.DbConnectionString.asString session.connectionInfo.DbConnectionString 19 | |] 20 | |> Should.haveSuccessfulExitCode 21 | 22 | TestDb.Should.notExist session 23 | 24 | module ``When db exist`` = 25 | open TestEnv 26 | open SqlDb 27 | 28 | [] 29 | let ``It should drop the database`` () = 30 | use session = TestDbSession.beginNew() 31 | 32 | session 33 | |> TestDb.create 34 | |> TestDb.Should.exist 35 | |> ignore 36 | 37 | SqlDb.Program.main [| 38 | "drop" 39 | "-c" 40 | MsSql.DbConnectionString.asString session.connectionInfo.DbConnectionString 41 | |] 42 | |> Should.haveSuccessfulExitCode 43 | 44 | TestDb.Should.notExist session -------------------------------------------------------------------------------- /src/tests/IntegrationTests/EnsureDbCommandTests.fs: -------------------------------------------------------------------------------- 1 | module ``Ensure Db exists command`` 2 | 3 | open Xunit 4 | 5 | module ``When db does not exist`` = 6 | open TestEnv 7 | open SqlDb 8 | 9 | [] 10 | let ``It should create the database`` () = 11 | use session = TestDbSession.beginNew() 12 | 13 | TestDb.Should.notExist session |> ignore 14 | 15 | SqlDb.Program.main [| 16 | "ensure" 17 | "-c" 18 | MsSql.DbConnectionString.asString session.connectionInfo.DbConnectionString 19 | |] 20 | |> Should.haveSuccessfulExitCode 21 | 22 | TestDb.Should.exist session 23 | 24 | module ``When db exist`` = 25 | open TestEnv 26 | open SqlDb 27 | 28 | [] 29 | let ``It should not re-create the database`` () = 30 | use session = TestDbSession.beginNew() 31 | 32 | session 33 | |> TestDb.create 34 | |> TestDb.createTable session.id 35 | |> ignore 36 | 37 | SqlDb.Program.main [| 38 | "ensure" 39 | "-c" 40 | MsSql.DbConnectionString.asString session.connectionInfo.DbConnectionString 41 | |] 42 | |> Should.haveSuccessfulExitCode 43 | 44 | session 45 | |> TestDb.Should.exist 46 | |> TestDb.Should.haveTable session.id -------------------------------------------------------------------------------- /src/tests/IntegrationTests/IntegrationTests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | true 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/tests/IntegrationTests/Should.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Should 3 | 4 | open Xunit.Sdk 5 | 6 | let beSuccessfulCommand (r: Result) = 7 | match r with 8 | | Ok v -> v 9 | | err -> raise (new XunitException(sprintf "Command was expected to be Ok, but got error: '%A'" err)) 10 | 11 | let haveSuccessfulExitCode (exitCode: int) = 12 | exitCode 13 | |> enum 14 | |> function 15 | | SqlDb.Program.ExitCode.Success -> () 16 | | v -> raise (new XunitException(sprintf "Program execution was expected to be Ok, but got exit code: '%A'" v)) 17 | -------------------------------------------------------------------------------- /src/tests/IntegrationTests/SqlScript/001_CreateTables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Foo (Id INT PRIMARY KEY) 2 | -------------------------------------------------------------------------------- /src/tests/IntegrationTests/TestEnv.fs: -------------------------------------------------------------------------------- 1 | module TestEnv 2 | 3 | open System.Data.SqlClient 4 | open SqlDb 5 | open System.Data 6 | open System 7 | 8 | type TestDbSession private (id, cnInfo:MsSql.ConnectionInfo, onComplete: TestDbSession -> unit) = 9 | member ___.id = id 10 | member ___.dbName = 11 | cnInfo.DbConnectionString 12 | |> MsSql.DbConnectionString.getDbName 13 | |> MsSql.DbName.asString 14 | 15 | member __.connectionInfo = cnInfo 16 | 17 | interface IDisposable with 18 | member this.Dispose() = 19 | onComplete(this) 20 | 21 | static member private cleanUp (session: TestDbSession) = 22 | use cn = new SqlConnection(session.connectionInfo.MasterConnectionString |> MsSql.MasterConnectionString.asString) 23 | cn.Open() 24 | try 25 | use command = cn.CreateCommand() 26 | command.CommandType <- CommandType.Text 27 | command.CommandText <- sprintf """ 28 | IF DB_ID(N'%s') IS NOT NULL BEGIN 29 | ALTER DATABASE [%s] SET SINGLE_USER WITH ROLLBACK IMMEDIATE 30 | DROP DATABASE IF EXISTS [%s] 31 | END""" session.dbName session.dbName session.dbName 32 | command.ExecuteNonQuery() |> ignore 33 | finally 34 | cn.Close() 35 | 36 | static member beginNew () = 37 | let id = Guid.NewGuid().ToString("N") 38 | let cnInfo = 39 | Config.getTestDbConnectionString() 40 | |> fun cnString -> 41 | let x = new SqlConnectionStringBuilder(cnString) 42 | x.InitialCatalog <- sprintf "%s_%s" x.InitialCatalog id 43 | x.ToString() 44 | |> MsSql.ConnectionInfo.fromConnectionString 45 | 46 | new TestDbSession(id, cnInfo, TestDbSession.cleanUp) 47 | 48 | module TestDb = 49 | let create (session: TestDbSession) = 50 | use cn = new SqlConnection(session.connectionInfo.MasterConnectionString |> MsSql.MasterConnectionString.asString) 51 | cn.Open() 52 | try 53 | use command = cn.CreateCommand() 54 | command.CommandType <- CommandType.Text 55 | command.CommandText <- sprintf """ 56 | CREATE DATABASE %s 57 | ALTER DATABASE %s SET RECOVERY SIMPLE""" session.dbName session.dbName 58 | command.ExecuteNonQuery() |> ignore 59 | finally 60 | cn.Close() 61 | 62 | session 63 | 64 | let createTable name (session: TestDbSession) = 65 | use cn = new SqlConnection(session.connectionInfo.DbConnectionString |> MsSql.DbConnectionString.asString) 66 | cn.Open() 67 | try 68 | use command = cn.CreateCommand() 69 | command.CommandType <- CommandType.Text 70 | command.CommandText <- sprintf "CREATE TABLE dbo.[%s] (Id int PRIMARY KEY) " name 71 | command.ExecuteNonQuery() |> ignore 72 | finally 73 | cn.Close() 74 | session 75 | 76 | [] 77 | module Should = 78 | open Xunit.Sdk 79 | 80 | let private dbExists (session: TestDbSession) = 81 | use cn = new SqlConnection(session.connectionInfo.MasterConnectionString |> MsSql.MasterConnectionString.asString) 82 | cn.Open() 83 | try 84 | use cmd = cn.CreateCommand() 85 | cmd.CommandType <- CommandType.Text 86 | cmd.CommandText <- "SELECT COALESCE(DB_ID(@dbName),-1)" 87 | cmd.Parameters.AddWithValue("dbName", session.dbName) |> ignore 88 | cmd.ExecuteScalar() 89 | finally 90 | cn.Close() 91 | |> fun r -> 92 | session.dbName,(r :?> int) <> -1 93 | 94 | let private tableExists tableName (session: TestDbSession) = 95 | use cn = new SqlConnection(session.connectionInfo.DbConnectionString |> MsSql.DbConnectionString.asString) 96 | cn.Open() 97 | try 98 | use cmd = cn.CreateCommand() 99 | cmd.CommandType <- CommandType.Text 100 | cmd.CommandText <- "SELECT COALESCE(OBJECT_ID(@tableName, N'U'),-1)" 101 | cmd.Parameters.AddWithValue("tableName", tableName) |> ignore 102 | cmd.ExecuteScalar() 103 | finally 104 | cn.Close() 105 | |> fun r -> 106 | tableName,(r :?> int) <> -1 107 | 108 | let exist session = 109 | session 110 | |> dbExists 111 | |> function 112 | | _, true -> session 113 | | dbName, false -> raise (XunitException (sprintf "Db '%A' was expected to exist." dbName)) 114 | 115 | let notExist session = 116 | session 117 | |> dbExists 118 | |> function 119 | | dbName, true -> raise (XunitException (sprintf "Db '%A' was not expected to exist." dbName)) 120 | | _, false -> session 121 | 122 | let haveTable name session = 123 | session 124 | |> tableExists name 125 | |> function 126 | | _, true -> session 127 | | tableName, false -> raise (XunitException (sprintf "Table '%A' was expected to exist." tableName)) -------------------------------------------------------------------------------- /src/tests/IntegrationTests/UpgradeDbCommandTests.fs: -------------------------------------------------------------------------------- 1 | module ``Upgrade Db command`` 2 | 3 | open Xunit 4 | 5 | module ``When clean db exist`` = 6 | open TestEnv 7 | open SqlDb 8 | 9 | [] 10 | let ``It should apply scripts from specified assembly`` () = 11 | use session = TestDbSession.beginNew() 12 | 13 | session 14 | |> TestDb.create 15 | |> TestDb.Should.exist 16 | |> ignore 17 | 18 | SqlDb.Program.main [| 19 | "up" 20 | "-c" 21 | MsSql.DbConnectionString.asString session.connectionInfo.DbConnectionString 22 | "-a" 23 | "IntegrationTests.dll" 24 | |] 25 | |> Should.haveSuccessfulExitCode 26 | 27 | session 28 | |> TestDb.Should.exist 29 | |> TestDb.Should.haveTable "SchemaVersions" 30 | |> TestDb.Should.haveTable "Foo" -------------------------------------------------------------------------------- /src/tests/UnitTests/CommonTests.fs: -------------------------------------------------------------------------------- 1 | module ``Command name`` 2 | 3 | open Xunit 4 | open SqlDb 5 | 6 | module ``When converting to a string`` = 7 | [] 8 | [] 9 | let ``It should return the string for`` (v) = 10 | CommandName v 11 | |> CommandName.asString 12 | |> Should.beEqual v 13 | -------------------------------------------------------------------------------- /src/tests/UnitTests/DbUpScriptTests.fs: -------------------------------------------------------------------------------- 1 | module ``DbUp script`` 2 | 3 | open Xunit 4 | open SqlDb 5 | 6 | let fooDb = MsSql.DbName.create "fooDb" 7 | 8 | module ``When creating drop Db script`` = 9 | 10 | [] 11 | let ``It should return a script with corresponding name`` () = 12 | DbUpScript.dropDb fooDb 13 | |> fun (n, _) -> n |> Should.beEqual "DbDropScript" 14 | 15 | module ``When creating ensure Db script`` = 16 | [] 17 | let ``It should return a script with corresponding name`` () = 18 | DbUpScript.ensureDbExists fooDb 19 | |> fun (n, _) -> n |> Should.beEqual "DbEnsureScript" 20 | -------------------------------------------------------------------------------- /src/tests/UnitTests/ErrorsTests.fs: -------------------------------------------------------------------------------- 1 | module ``Error message`` 2 | 3 | open Xunit 4 | open SqlDb 5 | open System 6 | 7 | module ``When converting to a string`` = 8 | [] 9 | let ``It should return the string`` () = 10 | let v = Guid.NewGuid().ToString("N") 11 | 12 | ErrorMessage v 13 | |> ErrorMessage.asString 14 | |> Should.beEqual v 15 | -------------------------------------------------------------------------------- /src/tests/UnitTests/LoggingTests.fs: -------------------------------------------------------------------------------- 1 | module ``Logging context`` 2 | 3 | open Xunit 4 | open SqlDb.Logging 5 | 6 | module ``When converting to a string`` = 7 | [] 8 | let ``It should return the string`` () = 9 | LoggingContexts.DbDropCommand 10 | |> LoggingContext.asString 11 | |> Should.beEqual "DbDropCommand" 12 | 13 | LoggingContexts.DbEnsureCommand 14 | |> LoggingContext.asString 15 | |> Should.beEqual "DbEnsureCommand" 16 | -------------------------------------------------------------------------------- /src/tests/UnitTests/Should.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Should 3 | 4 | open Xunit 5 | open System 6 | 7 | let beEqual<'a> (expected:'a) (actual:'a) = 8 | Assert.Equal(expected, actual) 9 | 10 | let beTrue (actual:bool) = 11 | Assert.True(actual) 12 | 13 | let beFalse (actual:bool) = 14 | Assert.False(actual) 15 | 16 | let throwInvalidArgs (f: unit -> 'a) = 17 | Assert.Throws((fun () -> f() |> ignore)) 18 | 19 | let throwInvalidArgsNamed paramName (f: unit -> 'a) = 20 | Assert.Throws(paramName, f >> ignore) 21 | -------------------------------------------------------------------------------- /src/tests/UnitTests/SqlDbTests.fs: -------------------------------------------------------------------------------- 1 | module ``Sql Db`` 2 | 3 | open Xunit 4 | open SqlDb 5 | 6 | [] 7 | let fooDbCnString = "Data Source=localhost;Initial Catalog=foodb;Integrated Security=True" 8 | 9 | [] 10 | let masterDbCnString = "Data Source=localhost;Initial Catalog=master;Integrated Security=True" 11 | 12 | module ``Db name`` = 13 | module ``When constructing`` = 14 | [] 15 | [] 16 | [] 17 | [] 18 | let ``It should throw for master db`` (v) = 19 | fun () -> MsSql.DbName.create v 20 | |> Should.throwInvalidArgsNamed "dbName" 21 | |> ignore 22 | 23 | [] 24 | [] 25 | [] 26 | [] 27 | let ``It should construct for non master db`` (v) = 28 | v 29 | |> MsSql.DbName.create 30 | |> MsSql.DbName.asString 31 | |> Should.beEqual v 32 | 33 | module ``Db connection string`` = 34 | module ``When constructing from connection string`` = 35 | [] 36 | let ``It should throw for master db`` () = 37 | fun () -> 38 | masterDbCnString 39 | |> MsSql.DbConnectionString.create 40 | |> Should.throwInvalidArgsNamed "dbName" 41 | 42 | [] 43 | let ``It should construct for non master db`` () = 44 | fooDbCnString 45 | |> MsSql.DbConnectionString.create 46 | |> fun cnString -> 47 | cnString 48 | |> MsSql.DbConnectionString.getDbName 49 | |> Should.beEqual (MsSql.DbName.create "foodb") 50 | 51 | cnString 52 | |> MsSql.DbConnectionString.asString 53 | |> Should.beEqual fooDbCnString 54 | 55 | module ``Master Db connection string`` = 56 | module ``When constructing from connection string`` = 57 | [] 58 | [] 59 | [] 60 | let ``It should switch to master db for non master db string and preserve master`` (v) = 61 | v 62 | |> MsSql.MasterConnectionString.create 63 | |> MsSql.MasterConnectionString.asString 64 | |> Should.beEqual masterDbCnString 65 | 66 | module ``Connection info`` = 67 | 68 | module ``When constructing from connection string`` = 69 | [] 70 | let ``It should throw, when master db is specified`` () = 71 | fun () -> 72 | masterDbCnString 73 | |> MsSql.ConnectionInfo.fromConnectionString 74 | |> Should.throwInvalidArgsNamed "dbName" 75 | 76 | [] 77 | let ``It should construct info from non master db connection string`` () = 78 | fooDbCnString 79 | |> MsSql.ConnectionInfo.fromConnectionString 80 | |> Should.beEqual { 81 | DbConnectionString = MsSql.DbConnectionString.create fooDbCnString 82 | MasterConnectionString = MsSql.MasterConnectionString.create masterDbCnString 83 | } -------------------------------------------------------------------------------- /src/tests/UnitTests/UnitTests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | false 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------