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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+