diff --git a/Build/build-scripts.ps1 b/Build/build-scripts.ps1 index b374dcb1..42c0589c 100644 --- a/Build/build-scripts.ps1 +++ b/Build/build-scripts.ps1 @@ -44,7 +44,7 @@ function Test-PowerShellCore($image) { --env connectionString=$connectionString ` --env test=/test ` $image ` - pwsh -Command ./test/Test.ps1 + pwsh -Command ./test/TestPowerShell.ps1 } } @@ -82,7 +82,7 @@ function Test-GlobalTool($image) { } } -function Test-NetCore($targetFramework, $image) { +function Test-NetCoreLinux($targetFramework, $image) { $bin = Join-Path $binDir "SqlDatabase\$targetFramework\publish" $app = $bin + ":/app" $test = $moduleIntegrationTests + ":/test" @@ -99,6 +99,17 @@ function Test-NetCore($targetFramework, $image) { } } +function Test-NetCore($targetFramework, $image) { + $bin = Join-Path $binDir "SqlDatabase\$targetFramework\publish" + $script = Join-Path $moduleIntegrationTests "Test.ps1" + + $builder = New-Object -TypeName System.Data.SqlClient.SqlConnectionStringBuilder -ArgumentList $connectionString + $builder["Data Source"] = "." + $cs = $builder.ToString() + + & $script $bin $cs +} + function Test-Unit($targetFramework) { $sourceDir = Join-Path $binDir "Tests" $sourceDir = Join-Path $sourceDir $targetFramework diff --git a/Build/build-tasks.ps1 b/Build/build-tasks.ps1 index 29fcbbb3..f2906c02 100644 --- a/Build/build-tasks.ps1 +++ b/Build/build-tasks.ps1 @@ -7,6 +7,7 @@ Task UnitTest -Depends InitializeTests ` , UnitTestcore22 ` , UnitTestcore31 ` , UnitTest50 + Task Test -Depends InitializeTests ` , TestPublishModule ` , TestPowerShellDesktop ` @@ -23,9 +24,13 @@ Task Test -Depends InitializeTests ` , TestPowerShellCore703 ` , TestPowerShellCore710 ` , TestPowerShellCore720 ` + , TestPowerShellCore712 ` , TestGlobalTool22 ` , TestGlobalTool31 ` , TestGlobalTool50 ` + , TestNetCoreLinux22 ` + , TestNetCoreLinux31 ` + , TestNetLinux50 ` , TestNetCore22 ` , TestNetCore31 ` , TestNet50 @@ -77,6 +82,7 @@ Task Build { New-Item -Path $net45Dest -ItemType Directory Copy-Item -Path (Join-Path $net45Source "SqlDatabase.exe") -Destination $net45Dest Copy-Item -Path (Join-Path $net45Source "SqlDatabase.pdb") -Destination $net45Dest + Copy-Item -Path (Join-Path $net45Source "System.Management.Automation.dll") -Destination $net45Dest } Task PackGlobalTool { @@ -234,6 +240,10 @@ Task TestPowerShellCore710 { Test-PowerShellCore "mcr.microsoft.com/powershell:7.1.0-ubuntu-18.04" } +Task TestPowerShellCore712 { + Test-PowerShellCore "mcr.microsoft.com/powershell:7.1.2-ubuntu-20.04" +} + Task TestPowerShellCore720 { Test-PowerShellCore "mcr.microsoft.com/powershell:7.2.0-preview.2-ubuntu-20.04" } @@ -251,31 +261,43 @@ Task TestPowerShellDesktop { $builder["Data Source"] = "." $env:connectionString = $builder.ToString() - $testScript = Join-Path $moduleIntegrationTests "Test.ps1" + $testScript = Join-Path $moduleIntegrationTests "TestPowerShell.ps1" Test-PowerShellDesktop ". $testScript" } Task TestGlobalTool22 { - Test-GlobalTool "microsoft/dotnet:2.2-sdk" + Test-GlobalTool "sqldatabase/dotnet_pwsh:2.2-sdk" } Task TestGlobalTool31 { - Test-GlobalTool "mcr.microsoft.com/dotnet/core/sdk:3.1" + Test-GlobalTool "sqldatabase/dotnet_pwsh:3.1-sdk" } Task TestGlobalTool50 { - Test-GlobalTool "mcr.microsoft.com/dotnet/sdk:5.0" + Test-GlobalTool "sqldatabase/dotnet_pwsh:5.0-sdk" +} + +Task TestNetCoreLinux22 { + Test-NetCoreLinux "netcoreapp2.2" "sqldatabase/dotnet_pwsh:2.2-runtime" +} + +Task TestNetCoreLinux31 { + Test-NetCoreLinux "netcoreapp3.1" "sqldatabase/dotnet_pwsh:3.1-runtime" +} + +Task TestNetLinux50 { + Test-NetCoreLinux "net5.0" "sqldatabase/dotnet_pwsh:5.0-runtime" } Task TestNetCore22 { - Test-NetCore "netcoreapp2.2" "microsoft/dotnet:2.2-runtime" + Test-NetCore "netcoreapp2.2" "sqldatabase/dotnet_pwsh:2.2-runtime" } Task TestNetCore31 { - Test-NetCore "netcoreapp3.1" "mcr.microsoft.com/dotnet/core/runtime:3.1" + Test-NetCore "netcoreapp3.1" "sqldatabase/dotnet_pwsh:3.1-runtime" } Task TestNet50 { - Test-NetCore "net5.0" "mcr.microsoft.com/dotnet/runtime:5.0" -} + Test-NetCore "net5.0" "sqldatabase/dotnet_pwsh:5.0-runtime" +} \ No newline at end of file diff --git a/Build/create-images-tasks.ps1 b/Build/create-images-tasks.ps1 new file mode 100644 index 00000000..bbe57987 --- /dev/null +++ b/Build/create-images-tasks.ps1 @@ -0,0 +1,60 @@ +Task default -Depends BuildDotnetSdk22 ` + , BuildDotnetRuntime22 ` + , BuildDotnetSdk31 ` + , BuildDotnetRuntime31 ` + , BuildDotnetSdk50 ` + , BuildDotnetRuntime50 + +Task BuildDotnetSdk22 { + Exec { + docker build ` + -f dotnet-sdk-2.2.dockerfile ` + -t sqldatabase/dotnet_pwsh:2.2-sdk ` + . + } +} + +Task BuildDotnetRuntime22 { + Exec { + docker build ` + -f dotnet-runtime-2.2.dockerfile ` + -t sqldatabase/dotnet_pwsh:2.2-runtime ` + . + } +} + +Task BuildDotnetSdk31 { + Exec { + docker build ` + -f dotnet-sdk-3.1.dockerfile ` + -t sqldatabase/dotnet_pwsh:3.1-sdk ` + . + } +} + +Task BuildDotnetRuntime31 { + Exec { + docker build ` + -f dotnet-runtime-3.1.dockerfile ` + -t sqldatabase/dotnet_pwsh:3.1-runtime ` + . + } +} + +Task BuildDotnetSdk50 { + Exec { + docker build ` + -f dotnet-sdk-5.0.dockerfile ` + -t sqldatabase/dotnet_pwsh:5.0-sdk ` + . + } +} + +Task BuildDotnetRuntime50 { + Exec { + docker build ` + -f dotnet-runtime-5.0.dockerfile ` + -t sqldatabase/dotnet_pwsh:5.0-runtime ` + . + } +} diff --git a/Build/create-images.ps1 b/Build/create-images.ps1 new file mode 100644 index 00000000..fc44a77a --- /dev/null +++ b/Build/create-images.ps1 @@ -0,0 +1,5 @@ +#Install-Module -Name psake +#Requires -Modules @{ModuleName='psake'; RequiredVersion='4.9.0'} + +$psakeMain = Join-Path $PSScriptRoot "create-images-tasks.ps1" +Invoke-psake $psakeMain \ No newline at end of file diff --git a/Build/dotnet-runtime-2.2.dockerfile b/Build/dotnet-runtime-2.2.dockerfile new file mode 100644 index 00000000..6609c060 --- /dev/null +++ b/Build/dotnet-runtime-2.2.dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/dotnet:2.2-runtime + +RUN curl -L https://github.com/PowerShell/PowerShell/releases/download/v6.2.7/powershell_6.2.7-1.debian.9_amd64.deb --output powershell_6.2.7-1.debian.9_amd64.deb && \ + dpkg -i powershell_6.2.7-1.debian.9_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_6.2.7-1.debian.9_amd64.deb \ No newline at end of file diff --git a/Build/dotnet-runtime-3.1.dockerfile b/Build/dotnet-runtime-3.1.dockerfile new file mode 100644 index 00000000..82ddfa70 --- /dev/null +++ b/Build/dotnet-runtime-3.1.dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/core/runtime:3.1 + +RUN apt-get update && \ + apt-get install -y liblttng-ust0 && \ + curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.0.5/powershell_7.0.5-1.debian.10_amd64.deb --output powershell_7.0.5-1.debian.10_amd64.deb && \ + dpkg -i powershell_7.0.5-1.debian.10_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_7.0.5-1.debian.10_amd64.deb \ No newline at end of file diff --git a/Build/dotnet-runtime-5.0.dockerfile b/Build/dotnet-runtime-5.0.dockerfile new file mode 100644 index 00000000..3cca76ca --- /dev/null +++ b/Build/dotnet-runtime-5.0.dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/runtime:5.0 + +RUN apt-get update && \ + apt-get install -y liblttng-ust0 curl && \ + curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.1.2/powershell_7.1.2-1.debian.10_amd64.deb --output powershell_7.1.2-1.debian.10_amd64.deb && \ + dpkg -i powershell_7.1.2-1.debian.10_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_7.1.2-1.debian.10_amd64.deb \ No newline at end of file diff --git a/Build/dotnet-sdk-2.2.dockerfile b/Build/dotnet-sdk-2.2.dockerfile new file mode 100644 index 00000000..e216a01c --- /dev/null +++ b/Build/dotnet-sdk-2.2.dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/dotnet:2.2-sdk + +RUN curl -L https://github.com/PowerShell/PowerShell/releases/download/v6.2.7/powershell_6.2.7-1.debian.9_amd64.deb --output powershell_6.2.7-1.debian.9_amd64.deb && \ + dpkg -i powershell_6.2.7-1.debian.9_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_6.2.7-1.debian.9_amd64.deb \ No newline at end of file diff --git a/Build/dotnet-sdk-3.1.dockerfile b/Build/dotnet-sdk-3.1.dockerfile new file mode 100644 index 00000000..b2e4803e --- /dev/null +++ b/Build/dotnet-sdk-3.1.dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 + +RUN apt-get update && \ + apt-get install -y liblttng-ust0 && \ + curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.0.5/powershell_7.0.5-1.debian.10_amd64.deb --output powershell_7.0.5-1.debian.10_amd64.deb && \ + dpkg -i powershell_7.0.5-1.debian.10_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_7.0.5-1.debian.10_amd64.deb \ No newline at end of file diff --git a/Build/dotnet-sdk-5.0.dockerfile b/Build/dotnet-sdk-5.0.dockerfile new file mode 100644 index 00000000..9ecbea75 --- /dev/null +++ b/Build/dotnet-sdk-5.0.dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/sdk:5.0 + +RUN apt-get update && \ + apt-get install -y liblttng-ust0 && \ + curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.1.2/powershell_7.1.2-1.debian.10_amd64.deb --output powershell_7.1.2-1.debian.10_amd64.deb && \ + dpkg -i powershell_7.1.2-1.debian.10_amd64.deb && \ + apt-get install -f && \ + rm -f powershell_7.1.2-1.debian.10_amd64.deb \ No newline at end of file diff --git a/Examples/PackageManagerConsole/SolutionScripts/SolutionScripts.csproj b/Examples/PackageManagerConsole/SolutionScripts/SolutionScripts.csproj index 98272266..6a8aab6b 100644 --- a/Examples/PackageManagerConsole/SolutionScripts/SolutionScripts.csproj +++ b/Examples/PackageManagerConsole/SolutionScripts/SolutionScripts.csproj @@ -10,7 +10,7 @@ - + diff --git a/Examples/PowerShellScript/example.ps1 b/Examples/PowerShellScript/example.ps1 new file mode 100644 index 00000000..da2ae77c --- /dev/null +++ b/Examples/PowerShellScript/example.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding(SupportsShouldProcess=$true)] # indicates that the script implementation supports -WhatIf scenario +param ( + $Command, # instance of SqlCommand, $null in case -WhatIf + $Variables # access to variables +) + +if (-not $Variables.TableName) { + throw "Variable TableName is not defined." +} + +if ($WhatIfPreference) { + # handle -WhatIf scenario + return +} + +Write-Information "start execution" + +$Command.CommandText = ("print 'current database name is {0}'" -f $Variables.DatabaseName) +$Command.ExecuteNonQuery() + +$Command.CommandText = ("drop table {0}" -f $Variables.TableName) +$Command.ExecuteNonQuery() + +Write-Information "finish execution" \ No newline at end of file diff --git a/Examples/PowerShellScript/readme.md b/Examples/PowerShellScript/readme.md new file mode 100644 index 00000000..c7240be9 --- /dev/null +++ b/Examples/PowerShellScript/readme.md @@ -0,0 +1,101 @@ +.ps1 script +========================================== + +SqlDatabase supports powershell scripts for commands [execute](https://github.com/max-ieremenko/SqlDatabase/tree/master/Examples/ExecuteScriptsFolder), [create](https://github.com/max-ieremenko/SqlDatabase/tree/master/Examples/CreateDatabaseFolder) and [upgrade](https://github.com/max-ieremenko/SqlDatabase/tree/master/Examples/MigrationStepsFolder). + +Example: + +```bash +$ SqlDatabase execute ^ + "-database=Data Source=server;Initial Catalog=database;Integrated Security=True" ^ + -from=c:\script.ps1 + +PS> Execute-SqlDatabase ` + -database "Data Source=server;Initial Catalog=database;Integrated Security=True" ` + -from c:\script.ps1 ` + -InformationAction Continue +``` + +script.ps1: + +```powershell +[CmdletBinding(SupportsShouldProcess=$true)] # indicates that the script implementation supports -WhatIf scenario +param ( + $Command, # instance of SqlCommand, $null in case -WhatIf + $Variables # access to variables +) + +if (-not $Variables.TableName) { + throw "Variable TableName is not defined." +} + +if ($WhatIfPreference) { + # handle -WhatIf scenario + return +} + +Write-Information "start execution" + +$Command.CommandText = ("print 'current database name is {0}'" -f $Variables.DatabaseName) +$Command.ExecuteNonQuery() + +$Command.CommandText = ("drop table {0}" -f $Variables.TableName) +$Command.ExecuteNonQuery() + +Write-Information "finish execution" +``` + +use + +* cmdlet parameter binding +* parameter `$Command` to affect database +* parameter `$Variables` to access variables +* `Write-*` to write something into output/log +* `SupportsShouldProcess=$true` and `$WhatIfPreference` if script supports `-WhatIf` scenario + +## Which version of PowerShell is used to run .ps1 + +### SqlDatabase powershell module + +[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/SqlDatabase.svg?style=flat-square)](https://www.powershellgallery.com/packages/SqlDatabase) + +The version with which you run the module. + +### .net framework 4.5.2 + +[![NuGet](https://img.shields.io/nuget/v/SqlDatabase.svg?style=flat-square&label=nuget%20net%204.5.2)](https://www.nuget.org/packages/SqlDatabase/) + +Installed Powershell Desktop version. + +### .net SDK tool for .net 5.0 or .net core 2.2/3.1 + +[![NuGet](https://img.shields.io/nuget/v/SqlDatabase.GlobalTool.svg?style=flat-square&label=nuget%20dotnet%20tool)](https://www.nuget.org/packages/SqlDatabase.GlobalTool/) + +Pre-installed Powershell Core is required, will be used by SqlDatabase as external component. Due to Powershell Core design, + +* SqlDatabase .net 5.0 can host Powershell Core versions below 7.2 +* .net core 3.1 below 7.1 +* .net core 2.2 below 7.0 + +PowerShell location can be passed via command line: + +```bash +$ SqlDatabase execute ^ + -usePowerShell=C:\Program Files\PowerShell\7 + +$ dotnet SqlDatabase.dll create ^ + -usePowerShell=/opt/microsoft/powershell/7 +``` + +PowerShell location by default: + +* if SqlDatabase is running by PowerShell (parent process is PowerShell) and version is compatible, use this version +* check well-known installation folders: `C:\Program Files\PowerShell` on windows and `/opt/microsoft/powershell` on linux, use latest compatible version + +## Scripts isolation + +For each .ps1 script, executing by SqlDatabase + +* create new PowerShell session with default cmdlets, providers, built-in functions, aliases etc. +* run script as `script block` +* destroy the session diff --git a/README.md b/README.md index f2440df7..43e808d8 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ Scripts ------- - *.sql* a text file with Sql Server scripts +- *.ps1* a text file with PowerShell script, details are [here](Examples/PowerShellScript) - *.dll* or *.exe* an .NET assembly with a script implementation, details are [here](Examples/CSharpMirationStep) [Back to ToC](#table-of-contents) diff --git a/Sources/Dependencies/System.Management.Automation.dll b/Sources/Dependencies/System.Management.Automation.dll new file mode 100644 index 00000000..3ca6528d Binary files /dev/null and b/Sources/Dependencies/System.Management.Automation.dll differ diff --git a/Sources/GlobalAssemblyInfo.cs b/Sources/GlobalAssemblyInfo.cs index e93db543..ede16238 100644 --- a/Sources/GlobalAssemblyInfo.cs +++ b/Sources/GlobalAssemblyInfo.cs @@ -4,10 +4,10 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SqlDatabase")] -[assembly: AssemblyCopyright("Copyright © 2018-2020 Max Ieremenko")] +[assembly: AssemblyCopyright("Copyright © 2018-2021 Max Ieremenko")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.1.3.0")] -[assembly: AssemblyFileVersion("2.1.3.0")] +[assembly: AssemblyVersion("2.2.0.0")] +[assembly: AssemblyFileVersion("2.2.0.0")] diff --git a/Sources/SqlDatabase.PowerShell.Test/SqlDatabase.PowerShell.Test.csproj b/Sources/SqlDatabase.PowerShell.Test/SqlDatabase.PowerShell.Test.csproj index d3b06cda..bf44d5ed 100644 --- a/Sources/SqlDatabase.PowerShell.Test/SqlDatabase.PowerShell.Test.csproj +++ b/Sources/SqlDatabase.PowerShell.Test/SqlDatabase.PowerShell.Test.csproj @@ -7,10 +7,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Sources/SqlDatabase.PowerShell/CmdletExtensions.cs b/Sources/SqlDatabase.PowerShell/CmdletExtensions.cs index f31aa3a5..d8697be0 100644 --- a/Sources/SqlDatabase.PowerShell/CmdletExtensions.cs +++ b/Sources/SqlDatabase.PowerShell/CmdletExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Management.Automation; using SqlDatabase.Configuration; diff --git a/Sources/SqlDatabase.PowerShell/ExecuteCmdLet.cs b/Sources/SqlDatabase.PowerShell/ExecuteCmdLet.cs index 73ec7263..68ac5779 100644 --- a/Sources/SqlDatabase.PowerShell/ExecuteCmdLet.cs +++ b/Sources/SqlDatabase.PowerShell/ExecuteCmdLet.cs @@ -1,5 +1,4 @@ -using System; -using System.Management.Automation; +using System.Management.Automation; using SqlDatabase.Configuration; namespace SqlDatabase.PowerShell diff --git a/Sources/SqlDatabase.PowerShell/SqlDatabaseCmdlet.cs b/Sources/SqlDatabase.PowerShell/SqlDatabaseCmdlet.cs index 55781de4..7d313871 100644 --- a/Sources/SqlDatabase.PowerShell/SqlDatabaseCmdlet.cs +++ b/Sources/SqlDatabase.PowerShell/SqlDatabaseCmdlet.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Management.Automation; using SqlDatabase.Configuration; diff --git a/Sources/SqlDatabase.Test/Commands/DatabaseCreateCommandTest.cs b/Sources/SqlDatabase.Test/Commands/DatabaseCreateCommandTest.cs index c73f24d8..af26f8d1 100644 --- a/Sources/SqlDatabase.Test/Commands/DatabaseCreateCommandTest.cs +++ b/Sources/SqlDatabase.Test/Commands/DatabaseCreateCommandTest.cs @@ -12,6 +12,8 @@ public class DatabaseCreateCommandTest private DatabaseCreateCommand _sut; private Mock _database; private Mock _scriptSequence; + private Mock _powerShellFactory; + private Mock _log; [SetUp] public void BeforeEachTest() @@ -22,15 +24,17 @@ public void BeforeEachTest() _scriptSequence = new Mock(MockBehavior.Strict); - var log = new Mock(MockBehavior.Strict); - log.Setup(l => l.Indent()).Returns((IDisposable)null); - log + _powerShellFactory = new Mock(MockBehavior.Strict); + + _log = new Mock(MockBehavior.Strict); + _log.Setup(l => l.Indent()).Returns((IDisposable)null); + _log .Setup(l => l.Error(It.IsAny())) .Callback(m => { Console.WriteLine("Error: {0}", m); }); - log + _log .Setup(l => l.Info(It.IsAny())) .Callback(m => { @@ -40,8 +44,9 @@ public void BeforeEachTest() _sut = new DatabaseCreateCommand { Database = _database.Object, - Log = log.Object, - ScriptSequence = _scriptSequence.Object + Log = _log.Object, + ScriptSequence = _scriptSequence.Object, + PowerShellFactory = _powerShellFactory.Object }; } @@ -64,6 +69,9 @@ public void ExecuteSequence() var step2 = new Mock(MockBehavior.Strict); step2.SetupGet(s => s.DisplayName).Returns("step 2"); + _powerShellFactory + .Setup(f => f.InitializeIfRequested(_log.Object)); + _database .Setup(d => d.Execute(step1.Object)) .Callback(() => _database.Setup(d => d.Execute(step2.Object))); @@ -74,6 +82,7 @@ public void ExecuteSequence() _database.VerifyAll(); _scriptSequence.VerifyAll(); + _powerShellFactory.VerifyAll(); } [Test] @@ -85,6 +94,9 @@ public void StopExecutionOnError() var step2 = new Mock(MockBehavior.Strict); step2.SetupGet(s => s.DisplayName).Returns("step 2"); + _powerShellFactory + .Setup(f => f.InitializeIfRequested(_log.Object)); + _database .Setup(d => d.Execute(step1.Object)) .Throws(); @@ -95,6 +107,7 @@ public void StopExecutionOnError() _database.VerifyAll(); _scriptSequence.VerifyAll(); + _powerShellFactory.VerifyAll(); } } } diff --git a/Sources/SqlDatabase.Test/Commands/DatabaseExecuteCommandTest.cs b/Sources/SqlDatabase.Test/Commands/DatabaseExecuteCommandTest.cs index b0eb5233..71211ab4 100644 --- a/Sources/SqlDatabase.Test/Commands/DatabaseExecuteCommandTest.cs +++ b/Sources/SqlDatabase.Test/Commands/DatabaseExecuteCommandTest.cs @@ -11,6 +11,8 @@ public class DatabaseExecuteCommandTest private DatabaseExecuteCommand _sut; private Mock _database; private Mock _scriptSequence; + private Mock _powerShellFactory; + private Mock _log; [SetUp] public void BeforeEachTest() @@ -21,9 +23,11 @@ public void BeforeEachTest() _scriptSequence = new Mock(MockBehavior.Strict); - var log = new Mock(MockBehavior.Strict); - log.Setup(l => l.Indent()).Returns((IDisposable)null); - log + _powerShellFactory = new Mock(MockBehavior.Strict); + + _log = new Mock(MockBehavior.Strict); + _log.Setup(l => l.Indent()).Returns((IDisposable)null); + _log .Setup(l => l.Info(It.IsAny())) .Callback(m => { @@ -33,8 +37,9 @@ public void BeforeEachTest() _sut = new DatabaseExecuteCommand { Database = _database.Object, - Log = log.Object, - ScriptSequence = _scriptSequence.Object + Log = _log.Object, + ScriptSequence = _scriptSequence.Object, + PowerShellFactory = _powerShellFactory.Object }; } @@ -47,6 +52,9 @@ public void ExecuteOneScript() var script2 = new Mock(MockBehavior.Strict); script2.SetupGet(s => s.DisplayName).Returns("step 2"); + _powerShellFactory + .Setup(f => f.InitializeIfRequested(_log.Object)); + _database .Setup(d => d.Execute(script1.Object)) .Callback(() => _database.Setup(d => d.Execute(script2.Object))); @@ -58,6 +66,7 @@ public void ExecuteOneScript() _database.VerifyAll(); script1.VerifyAll(); script2.VerifyAll(); + _powerShellFactory.VerifyAll(); } } } diff --git a/Sources/SqlDatabase.Test/Commands/DatabaseUpgradeCommandTest.cs b/Sources/SqlDatabase.Test/Commands/DatabaseUpgradeCommandTest.cs index 86b7c254..2a167312 100644 --- a/Sources/SqlDatabase.Test/Commands/DatabaseUpgradeCommandTest.cs +++ b/Sources/SqlDatabase.Test/Commands/DatabaseUpgradeCommandTest.cs @@ -11,6 +11,8 @@ public class DatabaseUpgradeCommandTest private DatabaseUpgradeCommand _sut; private Mock _database; private Mock _scriptSequence; + private Mock _powerShellFactory; + private Mock _log; [SetUp] public void BeforeEachTest() @@ -21,15 +23,17 @@ public void BeforeEachTest() _scriptSequence = new Mock(MockBehavior.Strict); - var log = new Mock(MockBehavior.Strict); - log.Setup(l => l.Indent()).Returns((IDisposable)null); - log + _powerShellFactory = new Mock(MockBehavior.Strict); + + _log = new Mock(MockBehavior.Strict); + _log.Setup(l => l.Indent()).Returns((IDisposable)null); + _log .Setup(l => l.Error(It.IsAny())) .Callback(m => { Console.WriteLine("Error: {0}", m); }); - log + _log .Setup(l => l.Info(It.IsAny())) .Callback(m => { @@ -39,8 +43,9 @@ public void BeforeEachTest() _sut = new DatabaseUpgradeCommand { Database = _database.Object, - Log = log.Object, - ScriptSequence = _scriptSequence.Object + Log = _log.Object, + ScriptSequence = _scriptSequence.Object, + PowerShellFactory = _powerShellFactory.Object }; } @@ -69,6 +74,9 @@ public void ExecuteSequence() var stepTo2 = new ScriptStep("module1", currentVersion, new Version("2.0"), updateTo2.Object); var stepTo3 = new ScriptStep("module2", new Version("2.0"), new Version("3.0"), updateTo3.Object); + _powerShellFactory + .Setup(f => f.InitializeIfRequested(_log.Object)); + _database .Setup(d => d.Execute(updateTo2.Object, "module1", stepTo2.From, stepTo2.To)) .Callback(() => _database.Setup(d => d.Execute(updateTo3.Object, "module2", stepTo3.From, stepTo3.To))); @@ -79,6 +87,7 @@ public void ExecuteSequence() _database.VerifyAll(); _scriptSequence.VerifyAll(); + _powerShellFactory.VerifyAll(); } [Test] @@ -95,6 +104,9 @@ public void StopExecutionOnError() var stepTo2 = new ScriptStep(string.Empty, currentVersion, new Version("2.0"), updateTo2.Object); var stepTo3 = new ScriptStep(string.Empty, new Version("2.0"), new Version("3.0"), updateTo3.Object); + _powerShellFactory + .Setup(f => f.InitializeIfRequested(_log.Object)); + _database.Setup(d => d.Execute(updateTo2.Object, string.Empty, stepTo2.From, stepTo2.To)).Throws(); _scriptSequence.Setup(s => s.BuildSequence()).Returns(new[] { stepTo2, stepTo3 }); @@ -103,6 +115,7 @@ public void StopExecutionOnError() _database.VerifyAll(); _scriptSequence.VerifyAll(); + _powerShellFactory.VerifyAll(); } } } diff --git a/Sources/SqlDatabase.Test/Configuration/CreateCommandLineTest.cs b/Sources/SqlDatabase.Test/Configuration/CreateCommandLineTest.cs index bcf19b00..0f6bf6f5 100644 --- a/Sources/SqlDatabase.Test/Configuration/CreateCommandLineTest.cs +++ b/Sources/SqlDatabase.Test/Configuration/CreateCommandLineTest.cs @@ -38,6 +38,9 @@ public void Parse() new Arg("varX", "1 2 3"), new Arg("varY", "value"), new Arg("configuration", "app.config"), +#if !NET472 + new Arg("usePowerShell", @"c:\PowerShell"), +#endif new Arg("whatIf"))); _sut.Scripts.ShouldBe(new[] { folder.Object }); @@ -52,6 +55,10 @@ public void Parse() _sut.ConfigurationFile.ShouldBe("app.config"); +#if !NET472 + _sut.UsePowerShell.ShouldBe(@"c:\PowerShell"); +#endif + _sut.WhatIf.ShouldBeTrue(); } @@ -60,6 +67,7 @@ public void CreateCommand() { _sut.WhatIf = true; _sut.Connection = new SqlConnectionStringBuilder(); + _sut.UsePowerShell = @"c:\PowerShell"; var actual = _sut .CreateCommand(_log.Object) @@ -69,7 +77,10 @@ public void CreateCommand() var database = actual.Database.ShouldBeOfType(); database.WhatIf.ShouldBeTrue(); - actual.ScriptSequence.ShouldBeOfType(); + var scriptFactory = actual.ScriptSequence.ShouldBeOfType().ScriptFactory.ShouldBeOfType(); + scriptFactory.PowerShellFactory.InstallationPath.ShouldBe(@"c:\PowerShell"); + + actual.PowerShellFactory.ShouldBe(scriptFactory.PowerShellFactory); } } } diff --git a/Sources/SqlDatabase.Test/Configuration/ExecuteCommandLineTest.cs b/Sources/SqlDatabase.Test/Configuration/ExecuteCommandLineTest.cs index 2b23fbec..9316d95a 100644 --- a/Sources/SqlDatabase.Test/Configuration/ExecuteCommandLineTest.cs +++ b/Sources/SqlDatabase.Test/Configuration/ExecuteCommandLineTest.cs @@ -44,6 +44,9 @@ public void Parse() new Arg("varY", "value"), new Arg("configuration", "app.config"), new Arg("transaction", "perStep"), +#if !NET472 + new Arg("usePowerShell", @"c:\PowerShell"), +#endif new Arg("whatIf"))); _sut.Scripts.Count.ShouldBe(2); @@ -62,6 +65,10 @@ public void Parse() _sut.Transaction.ShouldBe(TransactionMode.PerStep); +#if !NET472 + _sut.UsePowerShell.ShouldBe(@"c:\PowerShell"); +#endif + _sut.WhatIf.ShouldBeTrue(); } @@ -70,6 +77,7 @@ public void CreateCommand() { _sut.WhatIf = true; _sut.Connection = new SqlConnectionStringBuilder(); + _sut.UsePowerShell = @"c:\PowerShell"; var actual = _sut .CreateCommand(_log.Object) @@ -79,7 +87,10 @@ public void CreateCommand() var database = actual.Database.ShouldBeOfType(); database.WhatIf.ShouldBeTrue(); - actual.ScriptSequence.ShouldBeOfType(); + var scriptFactory = actual.ScriptSequence.ShouldBeOfType().ScriptFactory.ShouldBeOfType(); + scriptFactory.PowerShellFactory.InstallationPath.ShouldBe(@"c:\PowerShell"); + + actual.PowerShellFactory.ShouldBe(scriptFactory.PowerShellFactory); } } } \ No newline at end of file diff --git a/Sources/SqlDatabase.Test/Configuration/UpgradeCommandLineTest.cs b/Sources/SqlDatabase.Test/Configuration/UpgradeCommandLineTest.cs index afc39873..df5bb316 100644 --- a/Sources/SqlDatabase.Test/Configuration/UpgradeCommandLineTest.cs +++ b/Sources/SqlDatabase.Test/Configuration/UpgradeCommandLineTest.cs @@ -40,6 +40,9 @@ public void Parse() new Arg("configuration", "app.config"), new Arg("transaction", "perStep"), new Arg("folderAsModuleName"), +#if !NET472 + new Arg("usePowerShell", @"c:\PowerShell"), +#endif new Arg("whatIf"))); _sut.Scripts.ShouldBe(new[] { folder.Object }); @@ -56,6 +59,10 @@ public void Parse() _sut.Transaction.ShouldBe(TransactionMode.PerStep); +#if !NET472 + _sut.UsePowerShell.ShouldBe(@"c:\PowerShell"); +#endif + _sut.WhatIf.ShouldBeTrue(); _sut.FolderAsModuleName.ShouldBeTrue(); } @@ -66,6 +73,7 @@ public void CreateCommand() _sut.WhatIf = true; _sut.FolderAsModuleName = true; _sut.Connection = new SqlConnectionStringBuilder(); + _sut.UsePowerShell = @"c:\PowerShell"; var actual = _sut .CreateCommand(_log.Object) @@ -78,6 +86,11 @@ public void CreateCommand() var sequence = actual.ScriptSequence.ShouldBeOfType(); sequence.WhatIf.ShouldBeTrue(); sequence.FolderAsModuleName.ShouldBeTrue(); + + var scriptFactory = sequence.ScriptFactory.ShouldBeOfType(); + scriptFactory.PowerShellFactory.InstallationPath.ShouldBe(@"c:\PowerShell"); + + actual.PowerShellFactory.ShouldBe(scriptFactory.PowerShellFactory); } } } \ No newline at end of file diff --git a/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.ps1 b/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.ps1 new file mode 100644 index 00000000..e27d1601 --- /dev/null +++ b/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.ps1 @@ -0,0 +1,8 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + $Command, + $Variables +) + +$Command.CommandText = "CREATE SCHEMA demo" +$Command.ExecuteNonQuery() diff --git a/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.sql b/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.sql deleted file mode 100644 index a4e4ae63..00000000 --- a/Sources/SqlDatabase.Test/IntegrationTests/New/02.schemas.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE SCHEMA demo -GO diff --git a/Sources/SqlDatabase.Test/IntegrationTests/ProgramTest.cs b/Sources/SqlDatabase.Test/IntegrationTests/ProgramTest.cs index 917d1959..d0eae70f 100644 --- a/Sources/SqlDatabase.Test/IntegrationTests/ProgramTest.cs +++ b/Sources/SqlDatabase.Test/IntegrationTests/ProgramTest.cs @@ -25,6 +25,8 @@ public class ProgramTest [SetUp] public void BeforeEachTest() { + TestPowerShellHost.GetOrCreateFactory(); + _scriptsLocation = ConfigurationManager.AppSettings["IntegrationTestsScriptsLocation"]; if (!Path.IsPathRooted(_scriptsLocation)) { diff --git a/Sources/SqlDatabase.Test/IntegrationTests/Test.ps1 b/Sources/SqlDatabase.Test/IntegrationTests/Test.ps1 index a564d589..ab314bc9 100644 --- a/Sources/SqlDatabase.Test/IntegrationTests/Test.ps1 +++ b/Sources/SqlDatabase.Test/IntegrationTests/Test.ps1 @@ -1,35 +1,56 @@ -$ErrorActionPreference = "Stop" +param ( + $bin, + $connectionString +) -$connectionString = $env:connectionString -$test = $env:test +$ErrorActionPreference = "Stop" -Import-Module "SqlDatabase" +$app = Join-Path $bin "SqlDatabase.dll" +$scripts = Join-Path $PSScriptRoot "New" Write-Host "----- create new database ---" -Create-SqlDatabase ` - -database $connectionString ` - -from (Join-Path $test "New") ` - -var JohnCity=London,MariaCity=Paris +$scripts = Join-Path $PSScriptRoot "New" +Exec { + dotnet $app create ` + "-database=$connectionString" ` + "-from=$scripts" ` + -varJohnCity=London ` + -varMariaCity=Paris +} Write-Host "----- update database ---" -Upgrade-SqlDatabase ` - -database $connectionString ` - -from (Join-Path $test "Upgrade") ` - -var JohnSecondName=Smitt,MariaSecondName=X +$scripts = Join-Path $PSScriptRoot "Upgrade" +Exec { + dotnet $app upgrade ` + "-database=$connectionString" ` + "-from=$scripts" ` + -varJohnSecondName=Smitt ` + -varMariaSecondName=X +} Write-Host "----- update database (modularity) ---" -Upgrade-SqlDatabase ` - -database $connectionString ` - -from (Join-Path $test "UpgradeModularity") ` - -configuration (Join-Path $test "UpgradeModularity/SqlDatabase.exe.config") +$scripts = Join-Path $PSScriptRoot "UpgradeModularity" +$configuration = (Join-Path $scripts "SqlDatabase.exe.config") +Exec { + dotnet $app upgrade ` + "-database=$connectionString" ` + "-from=$scripts" ` + "-configuration=$configuration" +} Write-Host "----- export data ---" -Export-SqlDatabase ` - -database $connectionString ` - -from (Join-Path $test "Export/export.sql") ` - -toTable "dbo.ExportedData1" +$scripts = Join-Path $PSScriptRoot "Export/export.sql" +Exec { + dotnet $app export ` + "-database=$connectionString" ` + "-from=$scripts" ` + "-toTable=dbo.ExportedData1" +} Write-Host "----- execute script ---" -Execute-SqlDatabase ` - -database $connectionString ` - -from (Join-Path $test "execute/drop.database.sql") +$scripts = Join-Path $PSScriptRoot "execute/drop.database.sql" +Exec { + dotnet $app execute ` + "-database=$connectionString" ` + "-from=$scripts" +} \ No newline at end of file diff --git a/Sources/SqlDatabase.Test/IntegrationTests/Test.sh b/Sources/SqlDatabase.Test/IntegrationTests/Test.sh index 9a151f10..e43d0d70 100644 --- a/Sources/SqlDatabase.Test/IntegrationTests/Test.sh +++ b/Sources/SqlDatabase.Test/IntegrationTests/Test.sh @@ -1,3 +1,4 @@ +set -e echo "----- create new database ---" dotnet SqlDatabase.dll create \ "-database=$connectionString" \ diff --git a/Sources/SqlDatabase.Test/IntegrationTests/TestGlobalTool.sh b/Sources/SqlDatabase.Test/IntegrationTests/TestGlobalTool.sh index c5479016..afefb4a2 100644 --- a/Sources/SqlDatabase.Test/IntegrationTests/TestGlobalTool.sh +++ b/Sources/SqlDatabase.Test/IntegrationTests/TestGlobalTool.sh @@ -1,3 +1,4 @@ +set -e export PATH="$PATH:/root/.dotnet/tools" dotnet tool install -g --add-source $app SqlDatabase.GlobalTool --version $packageVersion diff --git a/Sources/SqlDatabase.Test/IntegrationTests/TestPowerShell.ps1 b/Sources/SqlDatabase.Test/IntegrationTests/TestPowerShell.ps1 new file mode 100644 index 00000000..a564d589 --- /dev/null +++ b/Sources/SqlDatabase.Test/IntegrationTests/TestPowerShell.ps1 @@ -0,0 +1,35 @@ +$ErrorActionPreference = "Stop" + +$connectionString = $env:connectionString +$test = $env:test + +Import-Module "SqlDatabase" + +Write-Host "----- create new database ---" +Create-SqlDatabase ` + -database $connectionString ` + -from (Join-Path $test "New") ` + -var JohnCity=London,MariaCity=Paris + +Write-Host "----- update database ---" +Upgrade-SqlDatabase ` + -database $connectionString ` + -from (Join-Path $test "Upgrade") ` + -var JohnSecondName=Smitt,MariaSecondName=X + +Write-Host "----- update database (modularity) ---" +Upgrade-SqlDatabase ` + -database $connectionString ` + -from (Join-Path $test "UpgradeModularity") ` + -configuration (Join-Path $test "UpgradeModularity/SqlDatabase.exe.config") + +Write-Host "----- export data ---" +Export-SqlDatabase ` + -database $connectionString ` + -from (Join-Path $test "Export/export.sql") ` + -toTable "dbo.ExportedData1" + +Write-Host "----- execute script ---" +Execute-SqlDatabase ` + -database $connectionString ` + -from (Join-Path $test "execute/drop.database.sql") diff --git a/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.ps1 b/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.ps1 new file mode 100644 index 00000000..36a9d82d --- /dev/null +++ b/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.ps1 @@ -0,0 +1,8 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + $Command, + $Variables +) + +$Command.CommandText = "ALTER TABLE demo.Person ADD SecondName NVARCHAR(250) NULL" +$Command.ExecuteNonQuery() diff --git a/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.sql b/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.sql deleted file mode 100644 index 89ef573d..00000000 --- a/Sources/SqlDatabase.Test/IntegrationTests/Upgrade/1.2_2.0.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE demo.Person ADD SecondName NVARCHAR(250) NULL -GO diff --git a/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.ps1 b/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.ps1 new file mode 100644 index 00000000..20131cd3 --- /dev/null +++ b/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.ps1 @@ -0,0 +1,11 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + $Command, + $Variables +) + +$Command.CommandText = "INSERT INTO moduleA.Person(Name) VALUES ('John'), ('Maria')" +$Command.ExecuteNonQuery() + +$Command.CommandText = "INSERT INTO moduleB.PersonAddress(PersonId, City) SELECT Person.Id, 'London' FROM demo.Person Person WHERE Person.Name = 'John'" +$Command.ExecuteNonQuery() diff --git a/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.sql b/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.sql deleted file mode 100644 index 51666d73..00000000 --- a/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.sql +++ /dev/null @@ -1,13 +0,0 @@ --- module dependency: moduleA 2.0 --- module dependency: moduleB 1.1 -GO - -INSERT INTO moduleA.Person(Name) -VALUES ('John'), ('Maria') -GO - -INSERT INTO moduleB.PersonAddress(PersonId, City) -SELECT Person.Id, 'London' -FROM demo.Person Person -WHERE Person.Name = 'John' -GO diff --git a/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.txt b/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.txt new file mode 100644 index 00000000..b80df4a4 --- /dev/null +++ b/Sources/SqlDatabase.Test/IntegrationTests/UpgradeModularity/moduleC_1.0_2.0.txt @@ -0,0 +1,2 @@ +-- module dependency: moduleA 2.0 +-- module dependency: moduleB 1.1 diff --git a/Sources/SqlDatabase.Test/Scripts/AssemblyScriptTest.cs b/Sources/SqlDatabase.Test/Scripts/AssemblyScriptTest.cs index bc76ea95..130e7d03 100644 --- a/Sources/SqlDatabase.Test/Scripts/AssemblyScriptTest.cs +++ b/Sources/SqlDatabase.Test/Scripts/AssemblyScriptTest.cs @@ -108,10 +108,12 @@ public void ExecuteWhatIf() [Test] public void GetDependencies() { - _sut.ReadDescriptionContent = () => Encoding.Default.GetBytes(@" + var description = Encoding.Default.GetBytes(@" -- module dependency: a 1.0 -- module dependency: b 1.0"); + _sut.ReadDescriptionContent = () => new MemoryStream(description); + var actual = _sut.GetDependencies(); actual.ShouldBe(new[] diff --git a/Sources/SqlDatabase.Test/Scripts/CreateScriptSequenceTest.cs b/Sources/SqlDatabase.Test/Scripts/CreateScriptSequenceTest.cs index f9a04c49..e42ba7a1 100644 --- a/Sources/SqlDatabase.Test/Scripts/CreateScriptSequenceTest.cs +++ b/Sources/SqlDatabase.Test/Scripts/CreateScriptSequenceTest.cs @@ -68,7 +68,8 @@ public void BuildSequenceFromOneFolder() .Concat(files) .ToArray(); - _sut.Sources.Add(FileFactory.Folder("root", content)); + _sut.Sources = new IFileSystemInfo[] { FileFactory.Folder("root", content) }; + var actual = _sut.BuildSequence(); // sorted A-Z, first files then folders @@ -88,9 +89,14 @@ public void BuildSequenceFromOneFolder() [Test] public void BuildSequenceFromFolderAndFile() { - _sut.Sources.Add(FileFactory.Folder("root", FileFactory.File("20.sql"), FileFactory.File("10.sql"))); - _sut.Sources.Add(FileFactory.File("02.sql")); - _sut.Sources.Add(FileFactory.File("01.sql")); + _sut.Sources = new IFileSystemInfo[] + { + FileFactory.Folder("root", FileFactory.File("20.sql"), FileFactory.File("10.sql")), + FileFactory.File("02.sql"), + FileFactory.File("01.sql"), + FileFactory.File("ignore") + }; + var actual = _sut.BuildSequence(); // sorted A-Z, first files then folders diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/DiagnosticsToolsTest.cs b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/DiagnosticsToolsTest.cs new file mode 100644 index 00000000..1397c3e5 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/DiagnosticsToolsTest.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using System.IO; +using NUnit.Framework; +using Shouldly; +using SqlDatabase.TestApi; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + [TestFixture] + public class DiagnosticsToolsTest + { + [Test] + public void ParseParentProcessIdLinux() + { + const string Content = "43 (dotnet) R 123 43 1 34816 43 4210688 1660 0 1 0 4 1 0 0 20 0 7 0 11192599 2821132288 5836 18446744073709551615 4194304 4261060 140729390742432 0 0 0 0 4096 17630 0 0 0 17 2 0 0 0 0 0 6360360 6362127 11538432 140729390743290 140729390743313 140729390743313 140729390743528 0"; + using (var file = new TempFile(".txt")) + { + File.WriteAllText(file.Location, Content); + + DiagnosticsTools.ParseParentProcessIdLinux(file.Location).ShouldBe(123); + } + } + +#if !NET472 + [Test] + public void GetParentProcessId() + { + DiagnosticsTools.GetParentProcessId(Process.GetCurrentProcess().Id).ShouldNotBeNull(); + } +#endif + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/InstallationSeekerTest.cs b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/InstallationSeekerTest.cs new file mode 100644 index 00000000..755bf2a8 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/InstallationSeekerTest.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Moq; +using NUnit.Framework; +using Shouldly; +using SqlDatabase.TestApi; +using InstallationInfo = SqlDatabase.Scripts.PowerShellInternal.InstallationSeeker.InstallationInfo; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + [TestFixture] + public class InstallationSeekerTest + { + [Test] + public void TryFindByParentProcess() + { + var actual = InstallationSeeker.TryFindByParentProcess(out var path); + if (actual) + { + Console.WriteLine(path); + } + } + + [Test] + public void TryFindOnDisk() + { +#if NET472 + Assert.Ignore(); +#endif + + InstallationSeeker.TryFindOnDisk(out var path).ShouldBeTrue(); + + Console.WriteLine(path); + } + + [Test] + public void TryGetInfo() + { + using (var dir = new TempDirectory()) + { + var root = Path.Combine(dir.Location, InstallationSeeker.RootAssemblyFileName); + + InstallationSeeker.TryGetInfo(dir.Location, out _).ShouldBeFalse(); + + File.WriteAllText(Path.Combine(dir.Location, "pwsh.dll"), "dummy"); + File.WriteAllText(root, "dummy"); + + InstallationSeeker.TryGetInfo(dir.Location, out _).ShouldBeFalse(); + + File.Delete(root); + File.Copy(GetType().Assembly.Location, root); + + InstallationSeeker.TryGetInfo(dir.Location, out var actual).ShouldBeTrue(); + + actual.Location.ShouldBe(dir.Location); + actual.Version.ShouldBe(GetType().Assembly.GetName().Version); + actual.ProductVersion.ShouldBe(actual.Version.ToString()); + } + } + + [Test] + [TestCaseSource(nameof(GetSortInstallationInfoCases))] + public void SortInstallationInfo(object item1, object item2) + { + var info1 = (InstallationInfo)item1; + var info2 = (InstallationInfo)item2; + + var comparer = new Mock>(MockBehavior.Strict); + comparer + .Setup(c => c.Equals(It.IsAny(), It.IsAny())) + .Returns((x, y) => + { + return x.Location.Equals(y.Location, StringComparison.OrdinalIgnoreCase) + && x.Version == y.Version + && x.ProductVersion.Equals(y.ProductVersion, StringComparison.OrdinalIgnoreCase); + }); + + var list = new List { info1, info2 }; + list.Sort(); + list[1].ShouldBe(info2, comparer.Object); + + list = new List { info2, info1 }; + list.Sort(); + list[1].ShouldBe(info2, comparer.Object); + } + + private static IEnumerable GetSortInstallationInfoCases() + { + yield return new TestCaseData( + new InstallationInfo("path", new Version(1, 0), "1.0"), + new InstallationInfo("path", new Version(2, 0), "2.0")) + { + TestName = "1.0 vs 2.0" + }; + + yield return new TestCaseData( + new InstallationInfo("path", new Version(1, 0), "1.0-preview"), + new InstallationInfo("path", new Version(1, 0), "1.0")) + { + TestName = "1.0-preview vs 1.0" + }; + + yield return new TestCaseData( + new InstallationInfo("path", new Version(1, 0), "1.0-preview.1"), + new InstallationInfo("path", new Version(1, 0), "1.0-preview.1")) + { + TestName = "1.0-preview.1 vs 1.0-preview.2" + }; + + yield return new TestCaseData( + new InstallationInfo("path", new Version(1, 0), "1.0-preview.1"), + new InstallationInfo("path", new Version(1, 0), "1.0-preview.2")) + { + TestName = "1.0-preview.1 vs 1.0-preview.2" + }; + + yield return new TestCaseData( + new InstallationInfo("path 1", new Version(1, 0), "1.0-preview.1"), + new InstallationInfo("path 2", new Version(1, 0), "1.0-preview.1")) + { + TestName = "1.0-preview.1 vs 1.0-preview.1" + }; + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfIgnore.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfIgnore.ps1 new file mode 100644 index 00000000..6e935766 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfIgnore.ps1 @@ -0,0 +1,6 @@ +param ( + $Command, + $Variables +) + +throw "not supported" diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfInvoke.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfInvoke.ps1 new file mode 100644 index 00000000..85f47063 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ExecuteWhatIfInvoke.ps1 @@ -0,0 +1,11 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + $Command, + $Variables +) + +if ($WhatIfPreference) { + Write-Host "WhatIf accepted" +} else { + throw "not supported" +} \ No newline at end of file diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleOutput.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleOutput.ps1 new file mode 100644 index 00000000..39a040c2 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleOutput.ps1 @@ -0,0 +1,5 @@ +Write-Host "hello from Write-Host" + +Write-Information "hello from Write-Information" + +Write-Warning "hello from Write-Warning" diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleThrow.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleThrow.ps1 new file mode 100644 index 00000000..86ace2bb --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleThrow.ps1 @@ -0,0 +1 @@ +throw "Ooops!" diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleWriteError.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleWriteError.ps1 new file mode 100644 index 00000000..2903e747 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.HandleWriteError.ps1 @@ -0,0 +1 @@ +Write-Error "hello from Write-Error" diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ParametersBinding.ps1 b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ParametersBinding.ps1 new file mode 100644 index 00000000..b330f231 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.ParametersBinding.ps1 @@ -0,0 +1,12 @@ +[CmdletBinding(SupportsShouldProcess=$true)] +param ( + $Command, + $Variables +) + +if ($WhatIfPreference) { + throw "not supported" +} + +$Command.CommandText = ("database name is {0}" -f $Variables.DatabaseName) +$Command.ExecuteNonQuery() diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.cs b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.cs new file mode 100644 index 00000000..7c4bcdd7 --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellInternal/PowerShellTest.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using Moq; +using NUnit.Framework; +using Shouldly; +using SqlDatabase.TestApi; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + [TestFixture] + public class PowerShellTest + { + private IPowerShellFactory _factory; + private IPowerShell _sut; + private Mock _logger; + private Mock _variables; + private Mock _command; + private List _logOutput; + + [OneTimeSetUp] + public void BeforeAllTests() + { + _logOutput = new List(); + + _logger = new Mock(MockBehavior.Strict); + _logger + .Setup(l => l.Info(It.IsNotNull())) + .Callback(m => _logOutput.Add("info: " + m)); + _logger + .Setup(l => l.Error(It.IsNotNull())) + .Callback(m => _logOutput.Add("error: " + m)); + _logger + .Setup(l => l.Indent()) + .Returns((IDisposable)null); + + _factory = TestPowerShellHost.GetOrCreateFactory(); + } + + [SetUp] + public void BeforeEachTest() + { + _logOutput.Clear(); + + _variables = new Mock(MockBehavior.Strict); + _command = new Mock(MockBehavior.Strict); + + _sut = _factory.Create(); + } + + [TearDown] + public void AfterEachTest() + { + foreach (var line in _logOutput) + { + Console.WriteLine(line); + } + } + + [Test] + [TestCase("PowerShellTest.ExecuteWhatIfInvoke.ps1", true)] + [TestCase("PowerShellTest.ExecuteWhatIfIgnore.ps1", false)] + public void SupportsShouldProcess(string scriptName, bool expected) + { + var script = LoadScript(scriptName); + + _sut.SupportsShouldProcess(script).ShouldBe(expected); + } + + [Test] + public void HandleOutput() + { + var script = LoadScript("PowerShellTest.HandleOutput.ps1"); + InvokeExecute(script, false); + + _logOutput.Count.ShouldBe(3); + _logOutput[0].ShouldBe("info: hello from Write-Host"); + _logOutput[1].ShouldBe("info: hello from Write-Information"); + _logOutput[2].ShouldBe("info: hello from Write-Warning"); + } + + [Test] + public void HandleWriteError() + { + var script = LoadScript("PowerShellTest.HandleWriteError.ps1"); + Assert.Throws(() => InvokeExecute(script, false)); + + _logOutput.Count.ShouldBe(1); + _logOutput[0].ShouldBe("error: hello from Write-Error"); + } + + [Test] + public void HandleThrow() + { + var script = LoadScript("PowerShellTest.HandleThrow.ps1"); + + var failed = false; + try + { + InvokeExecute(script, false); + } + catch (Exception ex) + { + failed = true; + Console.WriteLine(ex); + } + + failed.ShouldBeTrue(); + _logOutput.ShouldBeEmpty(); + } + + [Test] + public void ParametersBinding() + { + _command + .SetupProperty(c => c.CommandText); + _command + .Setup(c => c.ExecuteNonQuery()) + .Returns(0); + + _variables + .Setup(v => v.GetValue("DatabaseName")) + .Returns("test-db"); + + var script = LoadScript("PowerShellTest.ParametersBinding.ps1"); + + InvokeExecute(script, false); + + _command.Object.CommandText.ShouldBe("database name is test-db"); + _command.VerifyAll(); + } + + [Test] + public void ExecuteInvalidScript() + { + Assert.Throws(() => InvokeExecute("bla bla", false)); + + _logOutput.Count.ShouldBe(1); + _logOutput[0].ShouldStartWith("error: The term 'bla' is not recognized"); + } + + [Test] + public void ExecuteEmptyScript() + { + InvokeExecute(string.Empty, false); + + _logOutput.ShouldBeEmpty(); + } + + [Test] + public void ExecuteWhatIfInvoke() + { + var script = LoadScript("PowerShellTest.ExecuteWhatIfInvoke.ps1"); + + InvokeExecute(script, true); + + _logOutput.Count.ShouldBe(1); + _logOutput[0].ShouldBe("info: WhatIf accepted"); + } + + private static string LoadScript(string name) + { + var anchor = typeof(PowerShellTest); + using (var stream = anchor.Assembly.GetManifestResourceStream(anchor, name)) + { + stream.ShouldNotBeNull(name); + + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + + private void InvokeExecute(string script, bool whatIf) + { + var parameters = new KeyValuePair[2 + (whatIf ? 1 : 0)]; + parameters[0] = new KeyValuePair(PowerShellScript.ParameterCommand, _command.Object); + parameters[1] = new KeyValuePair(PowerShellScript.ParameterVariables, new VariablesProxy(_variables.Object)); + if (whatIf) + { + parameters[2] = new KeyValuePair(PowerShellScript.ParameterWhatIf, null); + } + + _sut.Invoke(script, _logger.Object, parameters); + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/PowerShellScriptTest.cs b/Sources/SqlDatabase.Test/Scripts/PowerShellScriptTest.cs new file mode 100644 index 00000000..448fd71b --- /dev/null +++ b/Sources/SqlDatabase.Test/Scripts/PowerShellScriptTest.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Text; +using Moq; +using NUnit.Framework; +using Shouldly; +using SqlDatabase.Scripts.PowerShellInternal; + +namespace SqlDatabase.Scripts +{ + [TestFixture] + public class PowerShellScriptTest + { + private PowerShellScript _sut; + private Mock _powerShell; + private Mock _variables; + private Mock _command; + private Mock _log; + + [SetUp] + public void BeforeEachTest() + { + _variables = new Mock(MockBehavior.Strict); + _command = new Mock(MockBehavior.Strict); + _powerShell = new Mock(MockBehavior.Strict); + _log = new Mock(MockBehavior.Strict); + + var factory = new Mock(MockBehavior.Strict); + factory + .Setup(f => f.Create()) + .Returns(_powerShell.Object); + + _sut = new PowerShellScript + { + PowerShellFactory = factory.Object + }; + } + + [Test] + public void Execute() + { + _powerShell + .Setup(p => p.Invoke("script content", _log.Object, It.IsNotNull[]>())) + .Callback[]>((_, _, parameters) => + { + parameters.Length.ShouldBe(2); + parameters[0].Key.ShouldBe(PowerShellScript.ParameterCommand); + parameters[0].Value.ShouldBe(_command.Object); + parameters[1].Key.ShouldBe(PowerShellScript.ParameterVariables); + parameters[1].Value.ShouldBeOfType(); + }); + + _sut.ReadScriptContent = () => new MemoryStream(Encoding.UTF8.GetBytes("script content")); + + _sut.Execute(_command.Object, _variables.Object, _log.Object); + + _powerShell.VerifyAll(); + } + + [Test] + public void ExecuteWhatIf() + { + _powerShell + .Setup(p => p.SupportsShouldProcess("script content")) + .Returns(true); + _powerShell + .Setup(p => p.Invoke("script content", _log.Object, It.IsNotNull[]>())) + .Callback[]>((_, _, parameters) => + { + parameters.Length.ShouldBe(3); + parameters[0].Key.ShouldBe(PowerShellScript.ParameterCommand); + parameters[0].Value.ShouldBeNull(); + parameters[1].Key.ShouldBe(PowerShellScript.ParameterVariables); + parameters[1].Value.ShouldBeOfType(); + parameters[2].Key.ShouldBe(PowerShellScript.ParameterWhatIf); + parameters[2].Value.ShouldBeNull(); + }); + + _sut.ReadScriptContent = () => new MemoryStream(Encoding.UTF8.GetBytes("script content")); + + _sut.Execute(null, _variables.Object, _log.Object); + + _powerShell.VerifyAll(); + } + + [Test] + public void ExecuteIgnoreWhatIf() + { + _powerShell + .Setup(p => p.SupportsShouldProcess("script content")) + .Returns(false); + _log + .Setup(l => l.Info(It.IsNotNull())); + + _sut.ReadScriptContent = () => new MemoryStream(Encoding.UTF8.GetBytes("script content")); + + _sut.Execute(null, _variables.Object, _log.Object); + + _powerShell.VerifyAll(); + _log.VerifyAll(); + } + + [Test] + public void GetDependencies() + { + var description = Encoding.Default.GetBytes(@" +-- module dependency: a 1.0 +-- module dependency: b 1.0"); + + _sut.ReadDescriptionContent = () => new MemoryStream(description); + + var actual = _sut.GetDependencies(); + + actual.ShouldBe(new[] + { + new ScriptDependency("a", new Version("1.0")), + new ScriptDependency("b", new Version("1.0")) + }); + } + + [Test] + public void GetDependenciesNoDescription() + { + _sut.ReadDescriptionContent = () => null; + + var actual = _sut.GetDependencies(); + + actual.ShouldBeEmpty(); + } + } +} diff --git a/Sources/SqlDatabase.Test/Scripts/ScriptFactoryTest.cs b/Sources/SqlDatabase.Test/Scripts/ScriptFactoryTest.cs index cdf6fea8..b50ee253 100644 --- a/Sources/SqlDatabase.Test/Scripts/ScriptFactoryTest.cs +++ b/Sources/SqlDatabase.Test/Scripts/ScriptFactoryTest.cs @@ -1,9 +1,11 @@ using System; using System.IO; using System.Text; +using Moq; using NUnit.Framework; using Shouldly; using SqlDatabase.Configuration; +using SqlDatabase.Scripts.PowerShellInternal; using SqlDatabase.TestApi; namespace SqlDatabase.Scripts @@ -12,19 +14,26 @@ namespace SqlDatabase.Scripts public class ScriptFactoryTest { private ScriptFactory _sut; + private Mock _powerShellFactory; private AppConfiguration _configuration; [SetUp] public void BeforeEachTest() { _configuration = new AppConfiguration(); - _sut = new ScriptFactory { Configuration = _configuration }; + _powerShellFactory = new Mock(MockBehavior.Strict); + + _sut = new ScriptFactory + { + Configuration = _configuration, + PowerShellFactory = _powerShellFactory.Object + }; } [Test] public void FromSqlFile() { - var file = FileFactory.File("11.sql", Encoding.UTF8.GetBytes("some script")); + var file = FileFactory.File("11.sql", "some script"); _sut.IsSupported(file.Name).ShouldBeTrue(); @@ -40,7 +49,7 @@ public void FromDllFile() var file = FileFactory.File( "11.dll", new byte[] { 1, 2, 3 }, - FileFactory.Folder("name", FileFactory.File("11.txt", new byte[] { 3, 2, 1 }))); + FileFactory.Folder("name", FileFactory.File("11.txt", "3, 2, 1"))); _sut.IsSupported(file.Name).ShouldBeTrue(); @@ -48,7 +57,7 @@ public void FromDllFile() script.DisplayName.ShouldBe("11.dll"); script.ReadAssemblyContent().ShouldBe(new byte[] { 1, 2, 3 }); - script.ReadDescriptionContent().ShouldBe(new byte[] { 3, 2, 1 }); + new StreamReader(script.ReadDescriptionContent()).ReadToEnd().ShouldBe("3, 2, 1"); } [Test] @@ -68,6 +77,39 @@ public void FromExeFile() script.ReadDescriptionContent().ShouldBeNull(); } + [Test] + public void FromPs1File() + { + var file = FileFactory.File( + "11.ps1", + "some script", + FileFactory.Folder("name", FileFactory.File("11.txt", "3, 2, 1"))); + + _powerShellFactory + .Setup(f => f.Request()); + + _sut.IsSupported(file.Name).ShouldBeTrue(); + + var script = _sut.FromFile(file).ShouldBeOfType(); + + script.PowerShellFactory.ShouldBe(_powerShellFactory.Object); + script.DisplayName.ShouldBe("11.ps1"); + new StreamReader(script.ReadScriptContent()).ReadToEnd().ShouldBe("some script"); + new StreamReader(script.ReadDescriptionContent()).ReadToEnd().ShouldBe("3, 2, 1"); + _powerShellFactory.VerifyAll(); + } + + [Test] + public void FromPs1FileNotSupported() + { + var file = FileFactory.File("11.ps1", "some script"); + _sut.PowerShellFactory = null; + + _sut.IsSupported(file.Name).ShouldBeFalse(); + + Assert.Throws(() => _sut.FromFile(file)); + } + [Test] public void FromFileNotSupported() { diff --git a/Sources/SqlDatabase.Test/Scripts/SqlBatchParserTest.cs b/Sources/SqlDatabase.Test/Scripts/SqlBatchParserTest.cs index c456bf1c..111217b7 100644 --- a/Sources/SqlDatabase.Test/Scripts/SqlBatchParserTest.cs +++ b/Sources/SqlDatabase.Test/Scripts/SqlBatchParserTest.cs @@ -37,7 +37,7 @@ public void IsGo(string line, bool expected) [TestCaseSource(nameof(GetExtractDependenciesTestCases))] public void ExtractDependencies(string sql, ScriptDependency[] expected) { - var actual = SqlBatchParser.ExtractDependencies(sql, "file name").ToArray(); + var actual = SqlBatchParser.ExtractDependencies(new StringReader(sql), "file name").ToArray(); actual.ShouldBe(expected); } @@ -47,7 +47,7 @@ public void ExtractDependencies(string sql, ScriptDependency[] expected) public void ExtractDependenciesInvalidVersion(string versionText) { var input = "-- module dependency: moduleName " + versionText; - var ex = Assert.Throws(() => SqlBatchParser.ExtractDependencies(input, "file name").ToArray()); + var ex = Assert.Throws(() => SqlBatchParser.ExtractDependencies(new StringReader(input), "file name").ToArray()); ex.Message.ShouldContain("moduleName"); ex.Message.ShouldContain(versionText); diff --git a/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj b/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj index d35c565d..e86af5f7 100644 --- a/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj +++ b/Sources/SqlDatabase.Test/SqlDatabase.Test.csproj @@ -8,22 +8,18 @@ - - - - - - - - - - + + + + + + + - - - + + @@ -34,7 +30,16 @@ + + + + + + + + + @@ -51,19 +56,4 @@ - - - - - - - - - - - - - - - diff --git a/Sources/SqlDatabase.Test/TestApi/FileFactory.cs b/Sources/SqlDatabase.Test/TestApi/FileFactory.cs index 7f514a80..e8fc1cbc 100644 --- a/Sources/SqlDatabase.Test/TestApi/FileFactory.cs +++ b/Sources/SqlDatabase.Test/TestApi/FileFactory.cs @@ -27,11 +27,14 @@ public static IFile File(string name, byte[] content, IFolder parent) public static IFile File(string name, byte[] content = null) => File(name, content, null); - public static IFile File(string name, string content) + public static IFile File(string name, string content) => File(name, content, null); + + public static IFile File(string name, string content, IFolder parent) { return File( name, - string.IsNullOrEmpty(content) ? new byte[0] : Encoding.UTF8.GetBytes(content)); + string.IsNullOrEmpty(content) ? new byte[0] : Encoding.UTF8.GetBytes(content), + parent); } public static IFolder Folder(string name, params IFileSystemInfo[] content) diff --git a/Sources/SqlDatabase.Test/TestApi/TestPowerShellHost.cs b/Sources/SqlDatabase.Test/TestApi/TestPowerShellHost.cs new file mode 100644 index 00000000..2d62658e --- /dev/null +++ b/Sources/SqlDatabase.Test/TestApi/TestPowerShellHost.cs @@ -0,0 +1,39 @@ +using System; +using Moq; +using SqlDatabase.Scripts; +using SqlDatabase.Scripts.PowerShellInternal; + +namespace SqlDatabase.TestApi +{ + internal static class TestPowerShellHost + { + public static IPowerShellFactory GetOrCreateFactory() + { + if (PowerShellFactory.SharedTestFactory == null) + { + PowerShellFactory.SharedTestFactory = CreateFactory(); + } + + return PowerShellFactory.SharedTestFactory; + } + + private static IPowerShellFactory CreateFactory() + { + var logger = new Mock(MockBehavior.Strict); + logger + .Setup(l => l.Info(It.IsNotNull())) + .Callback(m => Console.WriteLine("info: " + m)); + logger + .Setup(l => l.Error(It.IsNotNull())) + .Callback(m => Console.WriteLine("error: " + m)); + logger + .Setup(l => l.Indent()) + .Returns((IDisposable)null); + + var factory = PowerShellFactory.Create(null); + factory.Request(); + factory.InitializeIfRequested(logger.Object); + return factory; + } + } +} diff --git a/Sources/SqlDatabase/Commands/DatabaseCreateCommand.cs b/Sources/SqlDatabase/Commands/DatabaseCreateCommand.cs index 32f523a5..ae4874a9 100644 --- a/Sources/SqlDatabase/Commands/DatabaseCreateCommand.cs +++ b/Sources/SqlDatabase/Commands/DatabaseCreateCommand.cs @@ -8,6 +8,8 @@ internal sealed class DatabaseCreateCommand : DatabaseCommandBase { public ICreateScriptSequence ScriptSequence { get; set; } + public IPowerShellFactory PowerShellFactory { get; set; } + protected override void Greet(string databaseLocation) { Log.Info("Create {0}".FormatWith(databaseLocation)); @@ -21,6 +23,8 @@ protected override void ExecuteCore() throw new ConfigurationErrorsException("scripts to create database not found."); } + PowerShellFactory.InitializeIfRequested(Log); + foreach (var script in sequences) { var timer = Stopwatch.StartNew(); diff --git a/Sources/SqlDatabase/Commands/DatabaseExecuteCommand.cs b/Sources/SqlDatabase/Commands/DatabaseExecuteCommand.cs index 4deb9533..487bce8e 100644 --- a/Sources/SqlDatabase/Commands/DatabaseExecuteCommand.cs +++ b/Sources/SqlDatabase/Commands/DatabaseExecuteCommand.cs @@ -7,6 +7,8 @@ internal sealed class DatabaseExecuteCommand : DatabaseCommandBase { public ICreateScriptSequence ScriptSequence { get; set; } + public IPowerShellFactory PowerShellFactory { get; set; } + protected override void Greet(string databaseLocation) { Log.Info("Execute script on {0}".FormatWith(databaseLocation)); @@ -16,6 +18,8 @@ protected override void ExecuteCore() { var sequences = ScriptSequence.BuildSequence(); + PowerShellFactory.InitializeIfRequested(Log); + foreach (var script in sequences) { var timer = Stopwatch.StartNew(); diff --git a/Sources/SqlDatabase/Commands/DatabaseUpgradeCommand.cs b/Sources/SqlDatabase/Commands/DatabaseUpgradeCommand.cs index 0e1e8bfe..b872049c 100644 --- a/Sources/SqlDatabase/Commands/DatabaseUpgradeCommand.cs +++ b/Sources/SqlDatabase/Commands/DatabaseUpgradeCommand.cs @@ -10,6 +10,8 @@ internal sealed class DatabaseUpgradeCommand : DatabaseCommandBase { public IUpgradeScriptSequence ScriptSequence { get; set; } + public IPowerShellFactory PowerShellFactory { get; set; } + protected override void Greet(string databaseLocation) { Log.Info("Upgrade {0}".FormatWith(databaseLocation)); @@ -33,6 +35,8 @@ protected override void ExecuteCore() ShowMigrationSequenceFull(sequence); } + PowerShellFactory.InitializeIfRequested(Log); + foreach (var step in sequence) { var timer = Stopwatch.StartNew(); diff --git a/Sources/SqlDatabase/Configuration/Arg.cs b/Sources/SqlDatabase/Configuration/Arg.cs index c7b596d6..e945bdde 100644 --- a/Sources/SqlDatabase/Configuration/Arg.cs +++ b/Sources/SqlDatabase/Configuration/Arg.cs @@ -12,6 +12,7 @@ internal readonly struct Arg internal const string Configuration = "configuration"; internal const string Transaction = "transaction"; internal const string WhatIf = "whatIf"; + internal const string UsePowerShell = "usePowerShell"; internal const string FolderAsModuleName = "folderAsModuleName"; internal const string ExportToTable = "toTable"; diff --git a/Sources/SqlDatabase/Configuration/CommandLine.create.net452.txt b/Sources/SqlDatabase/Configuration/CommandLine.create.net452.txt new file mode 100644 index 00000000..abb35eba --- /dev/null +++ b/Sources/SqlDatabase/Configuration/CommandLine.create.net452.txt @@ -0,0 +1,34 @@ +Usage: SqlDatabase create [options]... + +Create a database + +[options] + -database: connection string to target database + "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" + + -from: a path to a folder or zip archive with sql scripts or path to a sql script file. Repeat -from to setup several sources. + -from=C:\MyDatabase\CreateScripts - create a new database from script files in CreateScripts folder + -from=C:\MyDatabase\CreateScripts.zip - create a new database from script files in CreateScripts.zip archive + -from=C:\MyDatabase.zip\CreateScripts - create a new database from script files in CreateScripts folder in MyDatabase.zip archive + -from=C:\MyDatabase\CreateScript.sql - create a new database from scripts in file CreateScript.sql + -from=C:\MyDatabase.zip\CreateScript.sql - create a new database from scripts in file CreateScript.sql in MyDatabase.zip archive + + -var: set a variable in format "-var[name of variable]=[value of variable]" + -varRecoveryModel=FULL - usage: ALTER DATABASE [{{NewDatabase}}] SET RECOVERY {{RecoveryModel}} WITH NO_WAIT + + -configuration: a path to application configuration file. Default is current SqlDatabase.exe.config. + -configuration=C:\MyDatabase\sql-database.config + + -whatIf: shows what would happen if the command runs. The command is not run. + +exit codes: + 0 - OK + 1 - invalid command line + 2 - errors during execution + +example: +create new "NewDatabase" on "server" based on scripts from "C:\NewDatabase" with "Variable1=value1" and "Variable2=value2" +> SqlDatabase create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 + +example: use previous example with -whatIf options to show steps without execution +> SqlDatabase create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.create.txt b/Sources/SqlDatabase/Configuration/CommandLine.create.txt index 75834064..a0f9f345 100644 --- a/Sources/SqlDatabase/Configuration/CommandLine.create.txt +++ b/Sources/SqlDatabase/Configuration/CommandLine.create.txt @@ -1,4 +1,4 @@ -Usage: SqlDatabase create [options]... +Usage: SqlDatabase create [options]... Create a database @@ -19,6 +19,9 @@ Create a database -configuration: a path to application configuration file. Default is current SqlDatabase.exe.config. -configuration=C:\MyDatabase\sql-database.config + -usePowerShell: a path to installation of PowerShell Core. PowerShell Core is required in case of running .ps1 scripts. + -usePowerShell=C:\Program Files\PowerShell\7 + -whatIf: shows what would happen if the command runs. The command is not run. exit codes: @@ -28,7 +31,7 @@ exit codes: example: create new "NewDatabase" on "server" based on scripts from "C:\NewDatabase" with "Variable1=value1" and "Variable2=value2" -> SqlDatabase.exe create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 +> SqlDatabase create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 example: use previous example with -whatIf options to show steps without execution -> SqlDatabase.exe create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file +> SqlDatabase create "-database=Data Source=server;Initial Catalog=NewDatabase;Integrated Security=True" -from=C:\NewDatabase -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.execute.net452.txt b/Sources/SqlDatabase/Configuration/CommandLine.execute.net452.txt new file mode 100644 index 00000000..f470e541 --- /dev/null +++ b/Sources/SqlDatabase/Configuration/CommandLine.execute.net452.txt @@ -0,0 +1,40 @@ +Usage: SqlDatabase execute [options]... + +Execute script(s) (file) + +[options] + -database: connection string to target database + "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" + + -from: a path to a folder or zip archive with sql scripts or path to a sql script file. Repeat -from to setup several sources. + -from=C:\MyDatabase\Scripts - execute migration script files on MyDatabase from Scripts folder + -from=C:\MyDatabase\Scripts.zip - execute script files on MyDatabase from Scripts.zip archive + -from=C:\MyDatabase.zip\Scripts - execute script files on MyDatabase from Scripts folder in MyDatabase.zip archive + -from=C:\MyDatabase\Script.sql - execute scripts on MyDatabase from file Script.sql + -from=C:\MyDatabase.zip\Script.sql - execute scripts on MyDatabase from file Script.sql in MyDatabase.zip archive + + -fromSql: an sql script text. Repeat -fromSql to setup several scripts. + "-fromSql=DROP TABLE schema.table" + + -var: set a variable in format "-var[name of variable]=[value of variable]" + -varSchema=dbo + -varTable=Table1 - usage: DROP TABLE [{{Schema}}].[{{Table}}] + + -transaction: transaction mode. Possible values: none, perStep. Default is none. + -transaction=perStep + + -configuration: path to application configuration file. Default is current SqlDatabase.exe.config. + -configuration=C:\MyDatabase\sql-database.config + + -whatIf: shows what would happen if the command runs. The command is not run. + +exit codes: + 0 - OK + 1 - invalid command line + 2 - errors during execution + +example: execute scripts from "c:\Scripts\script.sql" on "MyDatabase" on "server" with "Variable1=value1" and "Variable2=value2" +> SqlDatabase execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 + +example: use previous example with -whatIf options to show steps without execution +> SqlDatabase execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.execute.txt b/Sources/SqlDatabase/Configuration/CommandLine.execute.txt index 414613ed..bf53c04c 100644 --- a/Sources/SqlDatabase/Configuration/CommandLine.execute.txt +++ b/Sources/SqlDatabase/Configuration/CommandLine.execute.txt @@ -26,6 +26,9 @@ Execute script(s) (file) -configuration: path to application configuration file. Default is current SqlDatabase.exe.config. -configuration=C:\MyDatabase\sql-database.config + -usePowerShell: a path to installation of PowerShell Core. PowerShell Core is required in case of running .ps1 scripts. + -usePowerShell=C:\Program Files\PowerShell\7 + -whatIf: shows what would happen if the command runs. The command is not run. exit codes: @@ -34,7 +37,7 @@ exit codes: 2 - errors during execution example: execute scripts from "c:\Scripts\script.sql" on "MyDatabase" on "server" with "Variable1=value1" and "Variable2=value2" -> SqlDatabase.exe execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 +> SqlDatabase execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 example: use previous example with -whatIf options to show steps without execution -> SqlDatabase.exe execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file +> SqlDatabase execute "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=c:\Scripts\script.sql -varVariable1=value1 -varVariable2=value2 -whatIf \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.export.net452.txt b/Sources/SqlDatabase/Configuration/CommandLine.export.net452.txt new file mode 100644 index 00000000..9867b7c4 --- /dev/null +++ b/Sources/SqlDatabase/Configuration/CommandLine.export.net452.txt @@ -0,0 +1,37 @@ +Usage: SqlDatabase export [options]... + +Export data from a database to sql script file + +[options] + -database: connection string to the database + "-database=Data Source=server;Initial Catalog=master;Integrated Security=True" + + -fromSql: an sql script to select export data. Repeat -fromSql to setup several scripts. + "-fromSql=SELECT * FROM sys.tables" + "-fromSql=SELECT * FROM sys.tables ORDER BY name" + "-fromSql=select 1" + + -from: a path to a folder or zip archive with sql scripts or path to a sql script file. Repeat -from to setup several sources. + -from=C:\MyDatabase\Script.sql - execute data selections scripts from file Script.sql + + -toTable: setup "INSERT INTO" table name. Default is dbo.SqlDatabaseExport. + -toTable=#tempData + -toTable=schema.Table + + -toFile: write sql scripts into a file. By default write into standard output (console). + + -var: set a variable in format "-var[name of variable]=[value of variable]" + -varId=100 + -varSchema=dbo + -varTable=Table1 - usage: SELECT * FROM [{{Schema}}].[{{Table}}] WHERE Id > {{Id}} + + -configuration: path to application configuration file. Default is current SqlDatabase.exe.config. + -configuration=C:\MyDatabase\sql-database.config + +exit codes: + 0 - OK + 1 - invalid command line + 2 - errors during execution + +example: export data from sys.tables view into "c:\databases.sql" from "master" database on "server" +> SqlDatabase export "-database=Data Source=server;Initial Catalog=master;Integrated Security=True" "-fromSql=SELECT * FROM sys.tables" -toFile=c:\tables.sql \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.export.txt b/Sources/SqlDatabase/Configuration/CommandLine.export.txt index 0542e19d..9867b7c4 100644 --- a/Sources/SqlDatabase/Configuration/CommandLine.export.txt +++ b/Sources/SqlDatabase/Configuration/CommandLine.export.txt @@ -34,4 +34,4 @@ exit codes: 2 - errors during execution example: export data from sys.tables view into "c:\databases.sql" from "master" database on "server" -> SqlDatabase.exe export "-database=Data Source=server;Initial Catalog=master;Integrated Security=True" "-fromSql=SELECT * FROM sys.tables" -toFile=c:\tables.sql \ No newline at end of file +> SqlDatabase export "-database=Data Source=server;Initial Catalog=master;Integrated Security=True" "-fromSql=SELECT * FROM sys.tables" -toFile=c:\tables.sql \ No newline at end of file diff --git a/Sources/SqlDatabase/Configuration/CommandLine.upgrade.net452.txt b/Sources/SqlDatabase/Configuration/CommandLine.upgrade.net452.txt new file mode 100644 index 00000000..d33eeeda --- /dev/null +++ b/Sources/SqlDatabase/Configuration/CommandLine.upgrade.net452.txt @@ -0,0 +1,34 @@ +Usage: SqlDatabase upgrade [options]... + +Upgrade an existing database + +[options] + -database: connection string to target database + "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" + + -from: a path to a folder or zip archive with migration steps. Repeat -from to setup several sources. + -from=C:\MyDatabase\UpgradeScripts - execute migration steps on MyDatabase from UpgradeScripts folder + -from=C:\MyDatabase\UpgradeScripts.zip - execute migration steps on MyDatabase from UpgradeScripts.zip archive + -from=C:\MyDatabase.zip\UpgradeScripts - execute migration steps on MyDatabase from UpgradeScripts folder in MyDatabase.zip archive + + -var: set a variable in format "-var[name of variable]=[value of variable]" + -varRecoveryModel=FULL - usage: ALTER DATABASE [{{NewDatabase}}] SET RECOVERY {{RecoveryModel}} WITH NO_WAIT + + -transaction: transaction mode. Possible values: none, perStep. Default is none. + -transaction=perStep + + -configuration: path to application configuration file. Default is current SqlDatabase.exe.config. + -configuration=C:\MyDatabase\sql-database.config + + -whatIf: shows what would happen if the command runs. The command is not run. + +exit codes: + 0 - OK + 1 - invalid command line + 2 - errors during execution + +example: upgrade "MyDatabase" on "server", migration steps from "C:\MigrationSteps" with "Variable1=value1" and "Variable2=value2" +> SqlDatabase upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 + +example: use previous example with -whatIf options to show steps without execution +> SqlDatabase upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 -whatIf diff --git a/Sources/SqlDatabase/Configuration/CommandLine.upgrade.txt b/Sources/SqlDatabase/Configuration/CommandLine.upgrade.txt index 0d768bc5..0de17d64 100644 --- a/Sources/SqlDatabase/Configuration/CommandLine.upgrade.txt +++ b/Sources/SqlDatabase/Configuration/CommandLine.upgrade.txt @@ -20,6 +20,9 @@ Upgrade an existing database -configuration: path to application configuration file. Default is current SqlDatabase.exe.config. -configuration=C:\MyDatabase\sql-database.config + -usePowerShell: a path to installation of PowerShell Core. PowerShell Core is required in case of running .ps1 scripts. + -usePowerShell=C:\Program Files\PowerShell\7 + -whatIf: shows what would happen if the command runs. The command is not run. exit codes: @@ -28,7 +31,7 @@ exit codes: 2 - errors during execution example: upgrade "MyDatabase" on "server", migration steps from "C:\MigrationSteps" with "Variable1=value1" and "Variable2=value2" -> SqlDatabase.exe upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 +> SqlDatabase upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 example: use previous example with -whatIf options to show steps without execution -> SqlDatabase.exe upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 -whatIf +> SqlDatabase upgrade "-database=Data Source=server;Initial Catalog=MyDatabase;Integrated Security=True" -from=C:\MigrationSteps -varVariable1=value1 -varVariable2=value2 -whatIf diff --git a/Sources/SqlDatabase/Configuration/CreateCommandLine.cs b/Sources/SqlDatabase/Configuration/CreateCommandLine.cs index 76d4986a..1b403ce9 100644 --- a/Sources/SqlDatabase/Configuration/CreateCommandLine.cs +++ b/Sources/SqlDatabase/Configuration/CreateCommandLine.cs @@ -1,11 +1,14 @@ using System.Linq; using SqlDatabase.Commands; using SqlDatabase.Scripts; +using SqlDatabase.Scripts.PowerShellInternal; namespace SqlDatabase.Configuration { internal sealed class CreateCommandLine : CommandLineBase { + public string UsePowerShell { get; set; } + public bool WhatIf { get; set; } public override ICommand CreateCommand(ILogger logger) @@ -13,9 +16,15 @@ public override ICommand CreateCommand(ILogger logger) var configuration = new ConfigurationManager(); configuration.LoadFrom(ConfigurationFile); + var powerShellFactory = PowerShellFactory.Create(UsePowerShell); + var sequence = new CreateScriptSequence { - ScriptFactory = new ScriptFactory { Configuration = configuration.SqlDatabase }, + ScriptFactory = new ScriptFactory + { + Configuration = configuration.SqlDatabase, + PowerShellFactory = powerShellFactory + }, Sources = Scripts.ToArray() }; @@ -23,12 +32,21 @@ public override ICommand CreateCommand(ILogger logger) { Log = logger, Database = CreateDatabase(logger, configuration, TransactionMode.None, WhatIf), - ScriptSequence = sequence + ScriptSequence = sequence, + PowerShellFactory = powerShellFactory }; } protected override bool ParseArg(Arg arg) { +#if NETCOREAPP || NET5_0 + if (Arg.UsePowerShell.Equals(arg.Key, System.StringComparison.OrdinalIgnoreCase)) + { + UsePowerShell = arg.Value; + return true; + } +#endif + if (TryParseWhatIf(arg, out var value)) { WhatIf = value; diff --git a/Sources/SqlDatabase/Configuration/ExecuteCommandLine.cs b/Sources/SqlDatabase/Configuration/ExecuteCommandLine.cs index 8ae5f7d9..694d17c5 100644 --- a/Sources/SqlDatabase/Configuration/ExecuteCommandLine.cs +++ b/Sources/SqlDatabase/Configuration/ExecuteCommandLine.cs @@ -2,6 +2,7 @@ using System.Linq; using SqlDatabase.Commands; using SqlDatabase.Scripts; +using SqlDatabase.Scripts.PowerShellInternal; namespace SqlDatabase.Configuration { @@ -9,6 +10,8 @@ internal sealed class ExecuteCommandLine : CommandLineBase { public TransactionMode Transaction { get; set; } + public string UsePowerShell { get; set; } + public bool WhatIf { get; set; } public override ICommand CreateCommand(ILogger logger) @@ -16,9 +19,15 @@ public override ICommand CreateCommand(ILogger logger) var configuration = new ConfigurationManager(); configuration.LoadFrom(ConfigurationFile); + var powerShellFactory = PowerShellFactory.Create(UsePowerShell); + var sequence = new CreateScriptSequence { - ScriptFactory = new ScriptFactory { Configuration = configuration.SqlDatabase }, + ScriptFactory = new ScriptFactory + { + Configuration = configuration.SqlDatabase, + PowerShellFactory = powerShellFactory + }, Sources = Scripts.ToArray() }; @@ -26,7 +35,8 @@ public override ICommand CreateCommand(ILogger logger) { Log = logger, Database = CreateDatabase(logger, configuration, Transaction, WhatIf), - ScriptSequence = sequence + ScriptSequence = sequence, + PowerShellFactory = powerShellFactory }; } @@ -44,6 +54,13 @@ protected override bool ParseArg(Arg arg) return true; } +#if NETCOREAPP || NET5_0 + if (Arg.UsePowerShell.Equals(arg.Key, StringComparison.OrdinalIgnoreCase)) + { + UsePowerShell = arg.Value; + return true; + } +#endif if (TryParseWhatIf(arg, out var value)) { WhatIf = value; diff --git a/Sources/SqlDatabase/Configuration/UpgradeCommandLine.cs b/Sources/SqlDatabase/Configuration/UpgradeCommandLine.cs index 7dca5450..8ae6f4f0 100644 --- a/Sources/SqlDatabase/Configuration/UpgradeCommandLine.cs +++ b/Sources/SqlDatabase/Configuration/UpgradeCommandLine.cs @@ -2,6 +2,7 @@ using System.Linq; using SqlDatabase.Commands; using SqlDatabase.Scripts; +using SqlDatabase.Scripts.PowerShellInternal; using SqlDatabase.Scripts.UpgradeInternal; namespace SqlDatabase.Configuration @@ -10,6 +11,8 @@ internal sealed class UpgradeCommandLine : CommandLineBase { public TransactionMode Transaction { get; set; } + public string UsePowerShell { get; set; } + public bool FolderAsModuleName { get; set; } public bool WhatIf { get; set; } @@ -20,10 +23,15 @@ public override ICommand CreateCommand(ILogger logger) configuration.LoadFrom(ConfigurationFile); var database = CreateDatabase(logger, configuration, Transaction, WhatIf); + var powerShellFactory = PowerShellFactory.Create(UsePowerShell); var sequence = new UpgradeScriptSequence { - ScriptFactory = new ScriptFactory { Configuration = configuration.SqlDatabase }, + ScriptFactory = new ScriptFactory + { + Configuration = configuration.SqlDatabase, + PowerShellFactory = powerShellFactory + }, VersionResolver = new ModuleVersionResolver { Database = database, Log = logger }, Sources = Scripts.ToArray(), Log = logger, @@ -35,7 +43,8 @@ public override ICommand CreateCommand(ILogger logger) { Log = logger, Database = database, - ScriptSequence = sequence + ScriptSequence = sequence, + PowerShellFactory = powerShellFactory }; } @@ -53,6 +62,14 @@ protected override bool ParseArg(Arg arg) return true; } +#if NETCOREAPP || NET5_0 + if (Arg.UsePowerShell.Equals(arg.Key, StringComparison.OrdinalIgnoreCase)) + { + UsePowerShell = arg.Value; + return true; + } +#endif + if (TryParseSwitchParameter(arg, Arg.FolderAsModuleName, out value)) { FolderAsModuleName = value; diff --git a/Sources/SqlDatabase/Program.cs b/Sources/SqlDatabase/Program.cs index 3d8637d1..5a05e971 100644 --- a/Sources/SqlDatabase/Program.cs +++ b/Sources/SqlDatabase/Program.cs @@ -25,7 +25,7 @@ internal static int Run(ILogger logger, string[] args) if (factory.ShowCommandHelp) { - logger.Info(LoadHelpContent("CommandLine." + factory.ActiveCommandName + ".txt")); + logger.Info(LoadHelpContent(GetHelpFileName(factory.ActiveCommandName))); return ExitCode.InvalidCommandLine; } @@ -95,6 +95,16 @@ private static ILogger CreateLogger(string[] args) LoggerFactory.CreateDefault(); } + private static string GetHelpFileName(string commandName) + { +#if NET452 + const string Runtime = ".net452"; +#else + const string Runtime = null; +#endif + return "CommandLine." + commandName + Runtime + ".txt"; + } + private static string LoadHelpContent(string fileName) { var scope = typeof(ICommandLine); diff --git a/Sources/SqlDatabase/Scripts/AssemblyScript.cs b/Sources/SqlDatabase/Scripts/AssemblyScript.cs index 5d9d9d2e..38f16768 100644 --- a/Sources/SqlDatabase/Scripts/AssemblyScript.cs +++ b/Sources/SqlDatabase/Scripts/AssemblyScript.cs @@ -14,7 +14,7 @@ internal sealed class AssemblyScript : IScript public Func ReadAssemblyContent { get; set; } - public Func ReadDescriptionContent { get; set; } + public Func ReadDescriptionContent { get; set; } public AssemblyScriptConfiguration Configuration { get; set; } @@ -48,16 +48,17 @@ public IEnumerable ExecuteReader(IDbCommand command, IVariables var public IList GetDependencies() { - var description = ReadDescriptionContent(); - if (description == null || description.Length == 0) + using (var description = ReadDescriptionContent()) { - return new ScriptDependency[0]; - } + if (description == null) + { + return new ScriptDependency[0]; + } - using (var stream = new MemoryStream(description)) - using (var reader = new StreamReader(stream)) - { - return SqlBatchParser.ExtractDependencies(reader.ReadToEnd(), DisplayName).ToArray(); + using (var reader = new StreamReader(description)) + { + return SqlBatchParser.ExtractDependencies(reader, DisplayName).ToArray(); + } } } diff --git a/Sources/SqlDatabase/Scripts/CreateScriptSequence.cs b/Sources/SqlDatabase/Scripts/CreateScriptSequence.cs index 8001dda4..4347d546 100644 --- a/Sources/SqlDatabase/Scripts/CreateScriptSequence.cs +++ b/Sources/SqlDatabase/Scripts/CreateScriptSequence.cs @@ -7,7 +7,7 @@ namespace SqlDatabase.Scripts { internal sealed class CreateScriptSequence : ICreateScriptSequence { - public IList Sources { get; set; } = new List(); + public IList Sources { get; set; } public IScriptFactory ScriptFactory { get; set; } @@ -21,7 +21,7 @@ public IList BuildSequence() { Build(folder, null, ScriptFactory, result); } - else + else if (ScriptFactory.IsSupported(source.Name)) { result.Add(ScriptFactory.FromFile((IFile)source)); } diff --git a/Sources/SqlDatabase/Scripts/IPowerShellFactory.cs b/Sources/SqlDatabase/Scripts/IPowerShellFactory.cs new file mode 100644 index 00000000..39ca0756 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/IPowerShellFactory.cs @@ -0,0 +1,15 @@ +using SqlDatabase.Scripts.PowerShellInternal; + +namespace SqlDatabase.Scripts +{ + internal interface IPowerShellFactory + { + string InstallationPath { get; } + + void Request(); + + void InitializeIfRequested(ILogger logger); + + IPowerShell Create(); + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/DiagnosticsTools.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/DiagnosticsTools.cs new file mode 100644 index 00000000..6d06a5b4 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/DiagnosticsTools.cs @@ -0,0 +1,136 @@ +using System; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal static class DiagnosticsTools + { + public static bool IsOSPlatformWindows() + { +#if NET452 + return true; +#else + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#endif + } + + public static int? GetParentProcessId(int processId) + { +#if NET452 + return null; +#else + return IsOSPlatformWindows() ? GetParentProcessIdWindows(processId) : GetParentProcessIdLinux(processId); +#endif + } + + internal static int? ParseParentProcessIdLinux(string fileName) + { + string line = null; + + try + { + using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var reader = new StreamReader(stream, detectEncodingFromByteOrderMarks: true)) + { + line = reader.ReadLine(); + } + } + catch + { + } + + if (string.IsNullOrWhiteSpace(line)) + { + return null; + } + + // (2) comm %s: The filename of the executable, in parentheses. + var startIndex = line.LastIndexOf(')'); + if (startIndex <= 0 || startIndex >= line.Length) + { + return null; + } + + // (3) state %c + startIndex = line.IndexOf(' ', startIndex + 1); + if (startIndex <= 0 || startIndex >= line.Length) + { + return null; + } + + // (4) ppid %d: The PID of the parent of this process. + startIndex = line.IndexOf(' ', startIndex + 1); + if (startIndex <= 0 || startIndex >= line.Length) + { + return null; + } + + var endIndex = line.IndexOf(' ', startIndex + 1); + if (endIndex <= startIndex) + { + return null; + } + + var ppid = line.Substring(startIndex + 1, endIndex - startIndex - 1); + if (int.TryParse(ppid, NumberStyles.Any, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return null; + } + +#if !NET452 + private static int? GetParentProcessIdLinux(int processId) + { + // /proc/[pid]/stat https://man7.org/linux/man-pages/man5/procfs.5.html + var fileName = "/proc/" + processId.ToString(CultureInfo.InvariantCulture) + "/stat"; + + return ParseParentProcessIdLinux(fileName); + } + + private static int? GetParentProcessIdWindows(int processId) + { + const int DesiredAccess = 0x0400; // PROCESS_QUERY_INFORMATION + const int ProcessInfoClass = 0; // ProcessBasicInformation + + int? result = null; + using (var hProcess = OpenProcess(DesiredAccess, false, processId)) + { + if (!hProcess.IsInvalid) + { + var basicInformation = default(ProcessBasicInformation); + var pSize = 0; + var pbiSize = (uint)Marshal.SizeOf(); + + if (NtQueryInformationProcess(hProcess, ProcessInfoClass, ref basicInformation, pbiSize, ref pSize) == 0) + { + result = (int)basicInformation.InheritedFromUniqueProcessId; + } + } + } + + return result; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern Microsoft.Win32.SafeHandles.SafeProcessHandle OpenProcess(int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId); + + [DllImport("ntdll.dll")] + private static extern int NtQueryInformationProcess(Microsoft.Win32.SafeHandles.SafeProcessHandle hProcess, int pic, ref ProcessBasicInformation pbi, uint cb, ref int pSize); + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessBasicInformation + { + public uint ExitStatus; + public IntPtr PebBaseAddress; + public UIntPtr AffinityMask; + public int BasePriority; + public UIntPtr UniqueProcessId; + public UIntPtr InheritedFromUniqueProcessId; + } +#endif + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/IPowerShell.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/IPowerShell.cs new file mode 100644 index 00000000..897c238b --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/IPowerShell.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal interface IPowerShell + { + bool SupportsShouldProcess(string script); + + void Invoke(string script, ILogger logger, params KeyValuePair[] parameters); + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/InstallationSeeker.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/InstallationSeeker.cs new file mode 100644 index 00000000..67544a26 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/InstallationSeeker.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal static class InstallationSeeker + { + public const string RootAssemblyName = "System.Management.Automation"; + public const string RootAssemblyFileName = RootAssemblyName + ".dll"; + + public static bool TryFindByParentProcess(out string installationPath) + { + int processId; + DateTime processStartTime; + using (var current = Process.GetCurrentProcess()) + { + processId = current.Id; + processStartTime = current.StartTime; + } + + installationPath = FindPowerShellProcess(processId, processStartTime); + return !string.IsNullOrEmpty(installationPath) + && TryGetInfo(installationPath, out var info) + && IsCompatibleVersion(info.Version); + } + + public static bool TryFindOnDisk(out string installationPath) + { + installationPath = null; + var root = GetDefaultInstallationRoot(); + if (!Directory.Exists(root)) + { + return false; + } + + var candidates = new List(); + + var directories = Directory.GetDirectories(root); + for (var i = 0; i < directories.Length; i++) + { + if (TryGetInfo(directories[i], out var info) + && IsCompatibleVersion(info.Version)) + { + candidates.Add(info); + } + } + + if (candidates.Count == 0) + { + return false; + } + + candidates.Sort(); + installationPath = candidates[candidates.Count - 1].Location; + return true; + } + + public static bool TryGetInfo(string installationPath, out InstallationInfo info) + { + info = default; + + var root = Path.Combine(installationPath, RootAssemblyFileName); + if (!File.Exists(Path.Combine(installationPath, "pwsh.dll")) + || !File.Exists(root)) + { + return false; + } + + var fileInfo = FileVersionInfo.GetVersionInfo(root); + if (string.IsNullOrEmpty(fileInfo.FileVersion) + || string.IsNullOrEmpty(fileInfo.ProductVersion) + || !Version.TryParse(fileInfo.FileVersion, out var version)) + { + return false; + } + + info = new InstallationInfo(installationPath, version, fileInfo.ProductVersion); + return true; + } + + private static string FindPowerShellProcess(int processId, DateTime processStartTime) + { + var parentId = DiagnosticsTools.GetParentProcessId(processId); + if (!parentId.HasValue || parentId == processId) + { + return null; + } + + string parentLocation = null; + try + { + using (var parent = Process.GetProcessById(parentId.Value)) + { + if (parent.StartTime < processStartTime) + { + parentLocation = parent.MainModule?.FileName; + } + } + } + catch + { + } + + if (string.IsNullOrWhiteSpace(parentLocation) || !File.Exists(parentLocation)) + { + return null; + } + + var fileName = Path.GetFileName(parentLocation); + if (!"pwsh.exe".Equals(fileName, StringComparison.OrdinalIgnoreCase) && !"pwsh".Equals(fileName, StringComparison.OrdinalIgnoreCase)) + { + // try parent + return FindPowerShellProcess(parentId.Value, processStartTime); + } + + return Path.GetDirectoryName(parentLocation); + } + + private static string GetDefaultInstallationRoot() + { + if (DiagnosticsTools.IsOSPlatformWindows()) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "PowerShell"); + } + + return "/opt/microsoft/powershell"; + } + + private static bool IsCompatibleVersion(Version version) + { +#if NET5_0 + return version < new Version("7.2"); +#elif NETCOREAPP3_1_OR_GREATER + return version < new Version("7.1"); +#elif NETCOREAPP2_2_OR_GREATER + return version < new Version("7.0"); +#else + return false; +#endif + } + + [DebuggerDisplay("{Version}")] + internal readonly struct InstallationInfo : IComparable + { + public InstallationInfo(string location, Version version, string productVersion) + { + Location = location; + Version = version; + ProductVersion = productVersion ?? string.Empty; + } + + public string Location { get; } + + public Version Version { get; } + + public string ProductVersion { get; } + + public int CompareTo(InstallationInfo other) + { + var result = Version.CompareTo(other.Version); + if (result != 0) + { + return result; + } + + var isPreview = IsPreview(); + var otherIsPreview = other.IsPreview(); + if (isPreview && !otherIsPreview) + { + return -1; + } + + if (!isPreview && otherIsPreview) + { + return 1; + } + + result = StringComparer.InvariantCultureIgnoreCase.Compare(ProductVersion, other.ProductVersion); + if (result == 0) + { + result = StringComparer.InvariantCultureIgnoreCase.Compare(Location, other.Location); + } + + return result; + } + + private bool IsPreview() + { + return ProductVersion.IndexOf("preview", StringComparison.OrdinalIgnoreCase) > 0; + } + } + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShell.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShell.cs new file mode 100644 index 00000000..bf17a3a6 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShell.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal sealed class PowerShell : IPowerShell + { + public bool SupportsShouldProcess(string script) + { + var attributes = ScriptBlock.Create(script).Attributes; + for (var i = 0; i < attributes.Count; i++) + { + if (attributes[i] is CmdletBindingAttribute binding) + { + return binding.SupportsShouldProcess; + } + } + + return false; + } + + public void Invoke(string script, ILogger logger, params KeyValuePair[] parameters) + { + var sessionState = InitialSessionState.CreateDefault(); + using (var runSpace = RunspaceFactory.CreateRunspace(sessionState)) + { + runSpace.ThreadOptions = PSThreadOptions.UseCurrentThread; + runSpace.Open(); + + using (var powerShell = System.Management.Automation.PowerShell.Create()) + using (var listener = new PowerShellStreamsListener(powerShell.Streams, logger)) + { + powerShell.Runspace = runSpace; + + var ps = powerShell.AddScript(script); + for (var i = 0; i < parameters.Length; i++) + { + var p = parameters[i]; + if (p.Value == null) + { + ps = ps.AddParameter(p.Key); + } + else + { + ps.AddParameter(p.Key, p.Value); + } + } + + ps.Invoke(); + + if (listener.HasErrors) + { + throw new InvalidOperationException("Errors during script execution."); + } + } + } + } + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.cs new file mode 100644 index 00000000..5a96be36 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.cs @@ -0,0 +1,58 @@ +using System; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal sealed partial class PowerShellFactory : IPowerShellFactory + { + private bool _requested; + private bool _initialized; + + private PowerShellFactory(string installationPath) + { + InstallationPath = installationPath; + } + + public string InstallationPath { get; private set; } + + // only for tests + internal static IPowerShellFactory SharedTestFactory { get; set; } + + public static IPowerShellFactory Create(string installationPath) + { + if (SharedTestFactory != null) + { + return SharedTestFactory; + } + + return new PowerShellFactory(installationPath); + } + + public void Request() + { + _requested = true; + } + + public void InitializeIfRequested(ILogger logger) + { + if (_initialized || !_requested) + { + return; + } + + _initialized = true; + DoInitialize(logger); + } + + public IPowerShell Create() + { + if (!_initialized) + { + throw new InvalidOperationException("PowerShell host is not initialized."); + } + + return new PowerShell(); + } + + partial void DoInitialize(ILogger logger); + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.hosted.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.hosted.cs new file mode 100644 index 00000000..627543f3 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.hosted.cs @@ -0,0 +1,101 @@ +#if NETCOREAPP || NET5_0 +using System; +using System.IO; +using System.Management.Automation; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using SqlDatabase.Configuration; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + // https://github.com/PowerShell/PowerShell/tree/master/docs/host-powershell + internal partial class PowerShellFactory + { + partial void DoInitialize(ILogger logger) + { + if (string.IsNullOrEmpty(InstallationPath)) + { + if (InstallationSeeker.TryFindByParentProcess(out var test)) + { + InstallationPath = test; + } + else if (InstallationSeeker.TryFindOnDisk(out test)) + { + InstallationPath = test; + } + } + + if (string.IsNullOrEmpty(InstallationPath)) + { + throw new InvalidOperationException("PowerShell Core installation not found, please provide installation path via command line options {0}{1}.".FormatWith(Arg.Sign, Arg.UsePowerShell)); + } + + if (!InstallationSeeker.TryGetInfo(InstallationPath, out var info)) + { + throw new InvalidOperationException("PowerShell Core installation not found in {0}.".FormatWith(InstallationPath)); + } + + logger.Info("host PowerShell from {0}, version {1}".FormatWith(InstallationPath, info.ProductVersion)); + + AssemblyLoadContext.Default.Resolving += AssemblyResolving; + try + { + Test(logger); + } + catch (Exception ex) + { + throw new InvalidOperationException("PowerShell host initialization failed. Try to use another PowerShell Core installation.", ex); + } + finally + { + AssemblyLoadContext.Default.Resolving -= AssemblyResolving; + } + } + + private void Test(ILogger logger) + { + SetPowerShellAssemblyLoadContext(); + + using (logger.Indent()) + { + const string Script = @" +Write-Host ""PSVersion:"" $PSVersionTable.PSVersion +Write-Host ""PSEdition:"" $PSVersionTable.PSEdition +Write-Host ""OS:"" $PSVersionTable.OS"; + + Create().Invoke(Script, logger); + } + } + + private Assembly AssemblyResolving(AssemblyLoadContext context, AssemblyName assemblyName) + { + if (InstallationSeeker.RootAssemblyName.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + var fileName = Path.Combine(InstallationPath, InstallationSeeker.RootAssemblyFileName); + return context.LoadFromAssemblyPath(fileName); + } + + // https://github.com/PowerShell/PowerShell/releases/download/v7.0.5/powershell_7.0.5-1.debian.10_amd64.deb + // Could not load file or assembly 'Microsoft.Management.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. The system cannot find the file specified. + // package contains Microsoft.Management.Infrastructure, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null + if ("Microsoft.Management.Infrastructure".Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + var fileName = Path.Combine(InstallationPath, assemblyName.Name + ".dll"); + if (File.Exists(fileName)) + { + return context.LoadFromAssemblyPath(fileName); + } + } + + return null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void SetPowerShellAssemblyLoadContext() + { + PowerShellAssemblyLoadContextInitializer.SetPowerShellAssemblyLoadContext(InstallationPath); + } + } +} +#endif \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.native.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.native.cs new file mode 100644 index 00000000..b71e787b --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellFactory.native.cs @@ -0,0 +1,8 @@ +#if NET452 || NETSTANDARD +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal partial class PowerShellFactory + { + } +} +#endif \ No newline at end of file diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellStreamsListener.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellStreamsListener.cs new file mode 100644 index 00000000..eebaa85a --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/PowerShellStreamsListener.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Management.Automation; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal sealed class PowerShellStreamsListener : IDisposable + { + private readonly PSDataStreams _streams; + private readonly ILogger _logger; + private readonly IList _information; + + public PowerShellStreamsListener(PSDataStreams streams, ILogger logger) + { + _streams = streams; + _logger = logger; + + _information = GetInformation(streams); + + InvokeDataAdded(_information, OnInformation, true); + streams.Verbose.DataAdded += OnVerbose; + streams.Error.DataAdded += OnError; + streams.Warning.DataAdded += OnWarning; + } + + public bool HasErrors { get; private set; } + + public void Dispose() + { + _streams.Verbose.DataAdded -= OnVerbose; + _streams.Error.DataAdded -= OnError; + _streams.Warning.DataAdded -= OnWarning; + InvokeDataAdded(_information, OnInformation, false); + } + + private static IList GetInformation(PSDataStreams streams) + { +#if !NET452 + return streams.Information; +#else + return ReflectionGetInformation(streams); +#endif + } + + private static IList ReflectionGetInformation(PSDataStreams streams) + { + return (IList)streams + .GetType() + .FindProperty("Information") + .GetValue(streams, null); + } + + private static void InvokeDataAdded(object dataCollection, EventHandler handler, bool subscribe) + { + var evt = dataCollection + .GetType() + .FindEvent("DataAdded"); + + if (subscribe) + { + evt.AddMethod.Invoke(dataCollection, new object[] { handler }); + } + else + { + evt.RemoveMethod.Invoke(dataCollection, new object[] { handler }); + } + } + + private void OnWarning(object sender, DataAddedEventArgs e) + { + _logger.Info(_streams.Warning[e.Index]?.ToString()); + } + + private void OnError(object sender, DataAddedEventArgs e) + { + HasErrors = true; + _logger.Error(_streams.Error[e.Index]?.ToString()); + } + + private void OnVerbose(object sender, DataAddedEventArgs e) + { + _logger.Info(_streams.Verbose[e.Index]?.ToString()); + } + + private void OnInformation(object sender, DataAddedEventArgs e) + { + _logger.Info(_information[e.Index]?.ToString()); + } + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/ReflectionTools.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/ReflectionTools.cs new file mode 100644 index 00000000..25da1020 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/ReflectionTools.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal static class ReflectionTools + { + public static PropertyInfo FindProperty(this Type type, string name) + { + var result = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (result == null) + { + throw new InvalidOperationException( + "public property {0} not found in {1}.".FormatWith( + name, + type.FullName)); + } + + return result; + } + + public static EventInfo FindEvent(this Type type, string name) + { + var result = type.GetEvent(name, BindingFlags.Public | BindingFlags.Instance); + if (result?.AddMethod == null || result.RemoveMethod == null) + { + throw new InvalidOperationException("Event {0} not found in {1}.".FormatWith(name, type.FullName)); + } + + return result; + } + + public static T CreateDelegate(this MethodInfo method) + where T : Delegate + { + return (T)method.CreateDelegate(typeof(T)); + } + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellInternal/VariablesProxy.cs b/Sources/SqlDatabase/Scripts/PowerShellInternal/VariablesProxy.cs new file mode 100644 index 00000000..7c72b4b8 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellInternal/VariablesProxy.cs @@ -0,0 +1,20 @@ +using System.Dynamic; + +namespace SqlDatabase.Scripts.PowerShellInternal +{ + internal sealed class VariablesProxy : DynamicObject + { + private readonly IVariables _variables; + + public VariablesProxy(IVariables variables) + { + _variables = variables; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = _variables.GetValue(binder.Name); + return result != null; + } + } +} diff --git a/Sources/SqlDatabase/Scripts/PowerShellScript.cs b/Sources/SqlDatabase/Scripts/PowerShellScript.cs new file mode 100644 index 00000000..530fa197 --- /dev/null +++ b/Sources/SqlDatabase/Scripts/PowerShellScript.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using SqlDatabase.Scripts.PowerShellInternal; + +namespace SqlDatabase.Scripts +{ + internal sealed class PowerShellScript : IScript + { + public const string ParameterCommand = "Command"; + public const string ParameterVariables = "Variables"; + public const string ParameterWhatIf = "WhatIf"; + + public string DisplayName { get; set; } + + public Func ReadScriptContent { get; set; } + + public Func ReadDescriptionContent { get; set; } + + public IPowerShellFactory PowerShellFactory { get; set; } + + public void Execute(IDbCommand command, IVariables variables, ILogger logger) + { + string script; + using (var stream = ReadScriptContent()) + using (var reader = new StreamReader(stream)) + { + script = reader.ReadToEnd(); + } + + var powerShell = PowerShellFactory.Create(); + if (command == null) + { + InvokeWhatIf(powerShell, script, variables, logger); + } + else + { + Invoke(powerShell, script, command, variables, logger, false); + } + } + + public IEnumerable ExecuteReader(IDbCommand command, IVariables variables, ILogger logger) + { + throw new NotSupportedException("PowerShell script does not support readers."); + } + + public IList GetDependencies() + { + using (var description = ReadDescriptionContent()) + { + if (description == null) + { + return new ScriptDependency[0]; + } + + using (var reader = new StreamReader(description)) + { + return SqlBatchParser.ExtractDependencies(reader, DisplayName).ToArray(); + } + } + } + + private static void Invoke(IPowerShell powerShell, string script, IDbCommand command, IVariables variables, ILogger logger, bool whatIf) + { + var parameters = new KeyValuePair[2 + (whatIf ? 1 : 0)]; + parameters[0] = new KeyValuePair(ParameterCommand, command); + parameters[1] = new KeyValuePair(ParameterVariables, new VariablesProxy(variables)); + if (whatIf) + { + parameters[2] = new KeyValuePair(ParameterWhatIf, null); + } + + powerShell.Invoke(script, logger, parameters); + } + + private static void InvokeWhatIf(IPowerShell powerShell, string script, IVariables variables, ILogger logger) + { + if (powerShell.SupportsShouldProcess(script)) + { + Invoke(powerShell, script, null, variables, logger, true); + } + else + { + logger.Info("script does not support -WhatIf."); + } + } + } +} diff --git a/Sources/SqlDatabase/Scripts/ScriptFactory.cs b/Sources/SqlDatabase/Scripts/ScriptFactory.cs index 34b7fa63..9ec6411a 100644 --- a/Sources/SqlDatabase/Scripts/ScriptFactory.cs +++ b/Sources/SqlDatabase/Scripts/ScriptFactory.cs @@ -10,13 +10,16 @@ internal sealed class ScriptFactory : IScriptFactory { public AppConfiguration Configuration { get; set; } + public IPowerShellFactory PowerShellFactory { get; set; } + public bool IsSupported(string fileName) { var ext = Path.GetExtension(fileName); return ".sql".Equals(ext, StringComparison.OrdinalIgnoreCase) || ".exe".Equals(ext, StringComparison.OrdinalIgnoreCase) - || ".dll".Equals(ext, StringComparison.OrdinalIgnoreCase); + || ".dll".Equals(ext, StringComparison.OrdinalIgnoreCase) + || (PowerShellFactory != null && ".ps1".Equals(ext, StringComparison.OrdinalIgnoreCase)); } public IScript FromFile(IFile file) @@ -40,7 +43,25 @@ public IScript FromFile(IFile file) DisplayName = file.Name, Configuration = Configuration.AssemblyScript, ReadAssemblyContent = CreateBinaryReader(file), - ReadDescriptionContent = CreateAssemblyScriptDescriptionReader(file) + ReadDescriptionContent = CreateScriptDescriptionReader(file) + }; + } + + if (".ps1".Equals(ext, StringComparison.OrdinalIgnoreCase)) + { + if (PowerShellFactory == null) + { + throw new NotSupportedException(".ps1 scripts are not supported in this context."); + } + + PowerShellFactory.Request(); + + return new PowerShellScript + { + DisplayName = file.Name, + ReadScriptContent = file.OpenRead, + ReadDescriptionContent = CreateScriptDescriptionReader(file), + PowerShellFactory = PowerShellFactory }; } @@ -52,7 +73,7 @@ private static Func CreateBinaryReader(IFile file) return () => BinaryRead(file); } - private static Func CreateAssemblyScriptDescriptionReader(IFile file) + private static Func CreateScriptDescriptionReader(IFile file) { return () => { @@ -64,12 +85,8 @@ private static Func CreateAssemblyScriptDescriptionReader(IFile file) var descriptionName = Path.GetFileNameWithoutExtension(file.Name) + ".txt"; var description = parent.GetFiles().FirstOrDefault(i => string.Equals(descriptionName, i.Name, StringComparison.OrdinalIgnoreCase)); - if (description == null) - { - return null; - } - return BinaryRead(description); + return description?.OpenRead(); }; } diff --git a/Sources/SqlDatabase/Scripts/SqlBatchParser.cs b/Sources/SqlDatabase/Scripts/SqlBatchParser.cs index 4ad7f98f..43305b4e 100644 --- a/Sources/SqlDatabase/Scripts/SqlBatchParser.cs +++ b/Sources/SqlDatabase/Scripts/SqlBatchParser.cs @@ -41,27 +41,19 @@ public static IEnumerable SplitByGo(Stream sql) } } - public static IEnumerable ExtractDependencies(string sql, string scriptName) + public static IEnumerable ExtractDependencies(TextReader reader, string scriptName) { - if (string.IsNullOrWhiteSpace(sql)) + string line; + while ((line = reader.ReadLine()) != null) { - yield break; - } - - using (var reader = new StringReader(sql)) - { - string line; - while ((line = reader.ReadLine()) != null) + if (TryParseDependencyLine(line, out var moduleName, out var versionText)) { - if (TryParseDependencyLine(line, out var moduleName, out var versionText)) + if (!Version.TryParse(versionText, out var version)) { - if (!Version.TryParse(versionText, out var version)) - { - throw new InvalidOperationException("The current version value [{0}] of module [{1}] is invalid, script {2}.".FormatWith(versionText, moduleName, scriptName)); - } - - yield return new ScriptDependency(moduleName, version); + throw new InvalidOperationException("The current version value [{0}] of module [{1}] is invalid, script {2}.".FormatWith(versionText, moduleName, scriptName)); } + + yield return new ScriptDependency(moduleName, version); } } } diff --git a/Sources/SqlDatabase/Scripts/TextScript.cs b/Sources/SqlDatabase/Scripts/TextScript.cs index e1cf894a..d8b2e139 100644 --- a/Sources/SqlDatabase/Scripts/TextScript.cs +++ b/Sources/SqlDatabase/Scripts/TextScript.cs @@ -48,7 +48,12 @@ public IList GetDependencies() batch = SqlBatchParser.SplitByGo(sql).FirstOrDefault(); } - return SqlBatchParser.ExtractDependencies(batch, DisplayName).ToArray(); + if (string.IsNullOrWhiteSpace(batch)) + { + return new ScriptDependency[0]; + } + + return SqlBatchParser.ExtractDependencies(new StringReader(batch), DisplayName).ToArray(); } private IEnumerable ResolveBatches(IVariables variables, ILogger logger) diff --git a/Sources/SqlDatabase/Scripts/Variables.cs b/Sources/SqlDatabase/Scripts/Variables.cs index 23eefffa..39f3023f 100644 --- a/Sources/SqlDatabase/Scripts/Variables.cs +++ b/Sources/SqlDatabase/Scripts/Variables.cs @@ -70,7 +70,7 @@ internal void SetValue(VariableSource source, string name, string value) } } - private struct VariableValue + private readonly struct VariableValue { public VariableValue(VariableSource source, string value) { diff --git a/Sources/SqlDatabase/SqlDatabase.csproj b/Sources/SqlDatabase/SqlDatabase.csproj index 60166376..2971f269 100644 --- a/Sources/SqlDatabase/SqlDatabase.csproj +++ b/Sources/SqlDatabase/SqlDatabase.csproj @@ -32,20 +32,20 @@ sqlserver sqlcmd migration-tool c-sharp command-line-tool miration-step sql-script sql-database database-migrations export-data - - - - - - - - - + + + + + + + + + @@ -56,6 +56,25 @@ + + ..\Dependencies\System.Management.Automation.dll + + + + + + + + + + + + + + + + +