diff --git a/PSTK.psd1 b/PSTK.psd1 index 8e328e0..47825d4 100644 --- a/PSTK.psd1 +++ b/PSTK.psd1 @@ -12,7 +12,7 @@ RootModule = 'PSTK.psm1' # Version number of this module.s -ModuleVersion = '1.2.2' +ModuleVersion = '1.2.3' # Supported PSEditions # CompatiblePSEditions = @() @@ -94,6 +94,7 @@ FunctionsToExport = @( "Import-CSVProperties", "Import-Function", "Import-Properties", + "Invoke-OracleCmd", "New-DynamicParameter", "Out-Hashtable", "Protect-WindowsCmdValue", @@ -114,6 +115,7 @@ FunctionsToExport = @( "Test-EnvironmentVariable", "Test-HTTPStatus", "Test-Object", + "Test-OracleConnection", "Test-Service", "Test-SQLConnection", "Update-File", @@ -161,10 +163,8 @@ PrivateData = @{ # ReleaseNotes of this module ReleaseNotes = @' -[1.2.2] -- Added new features -- Redesign Resolve-Boolean -- Fixed an issue with Resolve-URI causing it to only resolve the last restricted character of the list +[1.2.3] +- Added Oracle database utility functions '@ } # End of PSData hashtable diff --git a/Public/Compare-Version.ps1 b/Public/Compare-Version.ps1 index ae94def..190c6ef 100644 --- a/Public/Compare-Version.ps1 +++ b/Public/Compare-Version.ps1 @@ -22,7 +22,8 @@ function Compare-Version { File name: Compare-Version.ps1 Author: Florian Carrier Creation date: 19/10/2019 - Last modified: 19/10/2019 + Last modified: 10/02/2020 + WARNING In case of modified formatting, Compare-Version only checks the semantic versionned part #> [CmdletBinding ( SupportsShouldProcess = $true @@ -80,13 +81,13 @@ function Compare-Version { "semantic" { # Prepare version numbers for comparison try { - $VersionNumber = [System.Version]::Parse($Version) + $VersionNumber = [System.Version]::Parse($Version) } catch [FormatException] { Write-Log -Type "ERROR" -Object "The version number ""$Version"" does not match $Format numbering" return $false } try { - $ReferenceNumber = [System.Version]::Parse($Reference) + $ReferenceNumber = [System.Version]::Parse($Reference) } catch [FormatException] { Write-Log -Type "ERROR" -Object "The version number ""$Reference"" does not match $Format numbering" return $false @@ -101,36 +102,33 @@ function Compare-Version { } "modified" { if ($Operator -in ("eq", "ne")) { - # Build comparison command - $Command = """$Version"" -$Operator ""$Reference""" - Write-Log -Type "DEBUG" -Object $Command - # Execute comparison - $Result = Invoke-Expression -Command $Command - # Return comparison result - return $Result + # Compare strings as-is + $VersionNumber = $Version + $ReferenceNumber = $Reference } else { # Parse version numbers - $VersionNumbers = $Version.Split(".") - $ReferenceNumbers = $Reference.Split(".") - # Check comparison operator - if ($Operator -in ("gt", "ge")) { - # TODO implement - # for ($i = 0; $i -lt $Count; $i++) { - # if ($i -lt ($Count - 1)) { - # $Command = """$($VersionNumbers[$i])"" -ge ""$($ReferenceNumbers[$i])""" - # } else { - # $Command = """$($VersionNumbers[$i])"" -Operator ""$($ReferenceNumbers[$i])""" - # } - # Write-Log -Type "DEBUG" -Object $Command - # $Result = Invoke-Expression -Command $Command - # if ($Result -eq $false) { - # return $false - # } - # } - } elseif ($Operator -in ("lt", "le")) { - # TODO implement + $SemanticVersion = Select-String -InputObject $Version -Pattern '(\d+.\d+.\d+)(?=\D*)' | ForEach-Object { $_.Matches.Value } + try { + $VersionNumber = [System.Version]::Parse($SemanticVersion) + } catch [FormatException] { + Write-Log -Type "ERROR" -Object "The version number ""$Version"" does not match semantic numbering" + return $false + } + $SemanticReference = Select-String -InputObject $Reference -Pattern '(\d+.\d+.\d+)(?=\D*)' | ForEach-Object { $_.Matches.Value } + try { + $ReferenceNumber = [System.Version]::Parse($SemanticReference) + } catch [FormatException] { + Write-Log -Type "ERROR" -Object "The version number ""$Reference"" does not match semantic numbering" + return $false } } + # Build comparison command + $Command = """$VersionNumber"" -$Operator ""$ReferenceNumber""" + Write-Log -Type "DEBUG" -Object $Command + # Execute comparison + $Result = Invoke-Expression -Command $Command + # Return comparison result + return $Result } default { Write-Log -Type "ERROR" -Object "The $Format versionning format is not yet supported" diff --git a/Public/Invoke-OracleCmd.ps1 b/Public/Invoke-OracleCmd.ps1 new file mode 100644 index 0000000..c8e2e90 --- /dev/null +++ b/Public/Invoke-OracleCmd.ps1 @@ -0,0 +1,192 @@ +function Invoke-OracleCmd { + <# + .SYNOPSIS + Invoke Oracle SQL command + + .DESCRIPTION + Run a SQL command on an Oracle database + + .NOTES + File name: Invoke-OracleCmd.ps1 + Author: Florian Carrier + Creation date: 04/02/2020 + Last modified: 06/02/2020 + Dependencies: Invoke-OracleCmd requires Oracle Data Provider for .NET + + .LINK + https://www.powershellgallery.com/packages/PSTK + + .LINK + Invoke-SqlCmd + + .LINK + https://www.oracle.com/database/technologies/appdev/dotnet/odp.html + + .LINK + https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core + #> + [CmdletBinding ( + SupportsShouldProcess = $true + )] + Param ( + [Parameter ( + Position = 1, + Mandatory = $true, + HelpMessage = "Name of the database host" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Server")] + [System.String] + $Hostname, + [Parameter ( + Position = 2, + Mandatory = $true, + HelpMessage = "Database server port number" + )] + [ValidateNotNullOrEmpty ()] + [System.String] + $PortNumber, + [Parameter ( + Position = 3, + Mandatory = $true, + HelpMessage = "Name of the Oracle service" + )] + [ValidateNotNullOrEmpty ()] + [System.String] + $ServiceName, + [Parameter ( + Position = 4, + Mandatory = $true, + HelpMessage = "SQL query" + )] + [ValidateNotNullOrEmpty ()] + [System.String] + $Query, + [Parameter ( + Position = 5, + Mandatory = $false, + HelpMessage = "Database user credentials", + ParameterSetName = "Credentials" + )] + [ValidateNotNullOrEmpty ()] + [System.Management.Automation.PSCredential] + $Credentials, + [Parameter ( + Position = 5, + Mandatory = $false, + HelpMessage = "User name", + ParameterSetName = "UserPassword" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Name")] + [System.String] + $Username, + [Parameter ( + Position = 6, + Mandatory = $false, + HelpMessage = "Password", + ParameterSetName = "UserPassword" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Pw")] + [System.String] + $Password, + [Parameter ( + Mandatory = $false, + HelpMessage = "Connection timeout (in seconds)" + )] + [ValidateNotNullOrEmpty ()] + [System.Int32] + $ConnectionTimeOut, + [Parameter ( + Mandatory = $false, + HelpMessage = "Query timeout (in seconds)" + )] + [ValidateNotNullOrEmpty ()] + [System.Int32] + $QueryTimeOut, + [Parameter ( + HelpMessage = "Abort on error" + )] + [Switch] + $AbortOnError, + [Parameter ( + HelpMessage = "Encrypt connection" + )] + [Switch] + $EncryptConnection, + [Parameter ( + HelpMessage = "Include SQL user errors" + )] + [Switch] + $IncludeSqlUserErrors, + [Parameter ( + HelpMessage = "Out SQL errors" + )] + [Switch] + $OutputSqlErrors + ) + Begin { + # Get global preference variables + Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState + } + Process { + # Define connection string + $ConnectionString = "Data Source='(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=$Hostname)(PORT=$PortNumber))(CONNECT_DATA=(SERVICE_NAME=$ServiceName)))';" + # Check authentication mode + if ($PSBoundParameters.ContainsKey("Credentials")) { + # If "secured" credentials are provided + $ConnectionString = $ConnectionString + "User ID=$($Credentials.Username);Password=$($Credentials.GetNetworkCredential().Password);" + $SensitiveData = $Credentials.GetNetworkCredential().Password + } elseif ($PSBoundParameters.ContainsKey("Username") -And $PSBoundParameters.ContainsKey("Password")) { + # If plain text credentials are provided + if ($Username) { + $ConnectionString = $ConnectionString + "User ID=$Username;Password=$Password;" + $SensitiveData = $Password + } else { + Write-Log -Type "ERROR" -Message "Invalid username ""$Username""" -ExitCode 1 + } + } else { + # Else default to integrated security (Windows authentication) + Write-Log -Type "DEBUG" -Message "Using Integrated Security" + $ConnectionString = $ConnectionString + "Integrated Security=True;" + } + # Technical parameters (Min Pool Size=10;Connection Lifetime=120;Connection Timeout=60;Incr Pool Size=5;Decr Pool Size=2;) + if ($PSBoundParameters.ContainsKey("ConnectionTimeOut") -And $ConnectionTimeOut -ne $null) { + $ConnectionString = $ConnectionString + "Connection Timeout=$ConnectionTimeOut;" + } + # Create connection object + Write-Log -Type "DEBUG" -Object $ConnectionString -Obfuscate $SensitiveData + $Connection = New-Object -TypeName "Oracle.ManagedDataAccess.Client.OracleConnection" -ArgumentList $ConnectionString + # Try to open the connection + try { + $Connection.Open() + } catch { + Write-Log -Type "ERROR" -Object "Unable to reach database $($Hostname):$PortNumber/$ServiceName" + return $Error + } + # Create SQL command + $Command = $Connection.CreateCommand() + # TODO sanitize query + $Command.CommandText = $Query + Write-Log -Type "DEBUG" -Object $Command + # Execute command + try { + $Reader = $Command.ExecuteReader() + } catch { + Write-Log -Type "ERROR" -Object "Could not execute statement`n$Query" + return $Error + } + # Get result + $Result = $Reader.Read() + # Close connection + $Connection.Close() + # Check outcome + if ($Result) { + # TODO return actual result + return $Result + } else { + return $null + } + } +} diff --git a/Public/Test-OracleConnection.ps1 b/Public/Test-OracleConnection.ps1 new file mode 100644 index 0000000..24db1dc --- /dev/null +++ b/Public/Test-OracleConnection.ps1 @@ -0,0 +1,167 @@ +function Test-OracleConnection { + <# + .SYNOPSIS + Test Oracle database connection + + .DESCRIPTION + Check that an Oracle database connection is working. + + .PARAMETER Hostname + The host name parameter corresponds to the name of the database host. + + .PARAMETER PortNumber + The port number parameter corresponds to the port number of the database server. + + .PARAMETER ServiceName + The service name parameter corresponds to the name of the database service. + + .PARAMETER Credentials + The credentials parameter corresponds to the credentials of accoun to use in case of SQL authentication. + + .PARAMETER Username + The username parameter corresponds to the username of the account to use in case of SQL authentication. + + .PARAMETER Password + The password parameter corresponds to the password of the account to use in case of SQL authentication. + + .PARAMETER TimeOut + The optional time-out parameter corresponds to the time in seconds before the connection is deemed unresponsive. The default value is 3 seconds. + + .INPUTS + None. You cannot pipe objects to Test-OracleConnection. + + .OUTPUTS + Boolean. Test-OracleConnection returns a boolean depending on the result of the connection attempt. + + .NOTES + File name: Test-OracleConnection.ps1 + Author: Florian Carrier + Creation date: 03/02/2020 + Last modified: 04/02/2020 + Dependencies: Test-OracleConnection requires Oracle Data Provider for .NET + + .LINK + https://www.powershellgallery.com/packages/PSTK + + .LINK + https://www.oracle.com/database/technologies/appdev/dotnet/odp.html + + .LINK + https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core + + #> + [CmdletBinding ( + SupportsShouldProcess = $true + )] + Param ( + [Parameter ( + Position = 1, + Mandatory = $true, + HelpMessage = "Name of the database host" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Server")] + [System.String] + $Hostname, + [Parameter ( + Position = 2, + Mandatory = $true, + HelpMessage = "Database server port number" + )] + [ValidateNotNullOrEmpty ()] + [System.String] + $PortNumber, + [Parameter ( + Position = 3, + Mandatory = $true, + HelpMessage = "Name of the Oracle service" + )] + [ValidateNotNullOrEmpty ()] + [System.String] + $ServiceName, + [Parameter ( + Position = 4, + Mandatory = $false, + HelpMessage = "Database user credentials", + ParameterSetName = "Credentials" + )] + [ValidateNotNullOrEmpty ()] + [System.Management.Automation.PSCredential] + $Credentials, + [Parameter ( + Position = 4, + Mandatory = $false, + HelpMessage = "User name", + ParameterSetName = "UserPassword" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Name")] + [System.String] + $Username, + [Parameter ( + Position = 5, + Mandatory = $false, + HelpMessage = "Password", + ParameterSetName = "UserPassword" + )] + [ValidateNotNullOrEmpty ()] + [Alias ("Pw")] + [System.String] + $Password, + [Parameter ( + Position = 5, + Mandatory = $false, + HelpMessage = "Connection timeout (in seconds)", + ParameterSetName = "Credentials" + )] + [Parameter ( + Position = 6, + Mandatory = $false, + HelpMessage = "Connection timeout (in seconds)", + ParameterSetName = "UserPassword" + )] + [ValidateNotNullOrEmpty ()] + [System.Int32] + $TimeOut = 3 + ) + Begin { + # Get global preference variables + Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState + } + Process { + # Define connection string + $ConnectionString = "Data Source='(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=$Hostname)(PORT=$PortNumber))(CONNECT_DATA=(SERVICE_NAME=$ServiceName)))';" + # Check authentication mode + if ($PSBoundParameters.ContainsKey("Credentials")) { + # If "secured" credentials are provided + $ConnectionString = $ConnectionString + "User ID=$($Credentials.Username);Password=$($Credentials.GetNetworkCredential().Password);" + $SensitiveData = $Credentials.GetNetworkCredential().Password + } elseif ($PSBoundParameters.ContainsKey("Username") -And $PSBoundParameters.ContainsKey("Password")) { + # If plain text credentials are provided + if ($Username) { + $ConnectionString = $ConnectionString + "User ID=$Username;Password=$Password;" + $SensitiveData = $Password + } else { + Write-Log -Type "ERROR" -Message "Invalid username ""$Username""" -ExitCode 1 + } + } else { + # Else default to integrated security (Windows authentication) + Write-Log -Type "DEBUG" -Message "Using Integrated Security" + $ConnectionString = $ConnectionString + "Integrated Security=True;" + } + # Technical parameters (Min Pool Size=10;Connection Lifetime=120;Connection Timeout=60;Incr Pool Size=5;Decr Pool Size=2;) + $ConnectionString = $ConnectionString + "Connection Timeout=$TimeOut;" + # Create connection object + Write-Log -Type "DEBUG" -Object $ConnectionString -Obfuscate $SensitiveData + $Connection = New-Object -TypeName "Oracle.ManagedDataAccess.Client.OracleConnection" -ArgumentList $ConnectionString + # Try to open the connection + try { + $Connection.Open() + $Connection.Close() + return $true + } catch { + # If connection fails + return $false + } + } +} diff --git a/Public/Write-InsertOrUpdate.ps1 b/Public/Write-InsertOrUpdate.ps1 index 0946936..aba93a3 100644 --- a/Public/Write-InsertOrUpdate.ps1 +++ b/Public/Write-InsertOrUpdate.ps1 @@ -4,17 +4,28 @@ function Write-InsertOrUpdate { Write INSERT OR UPDATE SQL query .DESCRIPTION - Check if record key exists and insert or update + Check if record key exists and insert or update data in a table .PARAMETER Table The table parameter corresponds to the name of the table in which to insert or update records. + Remark: It is recommended to use the fully qualified table name. + .PARAMETER Fields The fields parameter corresponds to the list of table columns and their corresponding values. .PARAMETER PrimaryKey The primary key parameter corresponds to the column constituting the unique key identifier of the record in the specified table. + .PARAMETER Vendor + The optional vendor parameter corresponds to the database vendor used to define the syntax to use for the SQL statement. + + The available values are: + - Oracle: Oracle database + - SQLServer: Microsoft SQL Server database + + The default value is SQLServer because it is fully integrated with PowerShell and for legacy support purposes. + .PARAMETER Identity The identity switch allows identity fields to be modifed. @@ -33,9 +44,11 @@ function Write-InsertOrUpdate { File name: Write-InsertOrUpdate.ps1 Author: Florian Carrier Creation date: 15/10/2019 - Last modified: 17/10/2019 + Last modified: 26/02/2020 #> - [CmdletBinding ()] + [CmdletBinding ( + SupportsShouldProcess = $true + )] Param ( [Parameter ( Position = 1, @@ -62,6 +75,18 @@ function Write-InsertOrUpdate { [Alias ("PK")] [String[]] $PrimaryKey, + [Parameter ( + Position = 4, + Mandatory = $false, + HelpMessage = "Database vendor (syntax)" + )] + [ValidateSet ( + "Oracle", + "SQLServer" + )] + [Alias ("Syntax")] + [String] + $Vendor = "SQLServer", [Parameter ( HelpMessage = "Switch to enable working with identities" )] @@ -75,7 +100,6 @@ function Write-InsertOrUpdate { # Check that values are provided for the primary key foreach ($Key in $PrimaryKey) { if (Find-Key -Hashtable $Fields -Key $Key) { - Write-Log -Type "DEBUG" -Object $Fields.$Key if ($Fields.$Key -eq $null) { Write-Log -Type "ERROR" -Object "Missing value for primary key $Key" -ExitCode 1 } @@ -85,44 +109,84 @@ function Write-InsertOrUpdate { } } Process { - # Define existence check - foreach($Key in $PrimaryKey) { - if ($PrimaryKeyCheck -eq $null) { $PrimaryKeyCheck = " WHERE $Key = $($Fields.$Key)" } - else { $PrimaryKeyCheck += " AND $Key = $($Fields.$Key)" } - } - $Check = "IF EXISTS (SELECT COUNT(1) FROM $Table" + $PrimaryKeyCheck + ")" - - # Loop through fields - foreach ($Field in $Fields.GetEnumerator()) { - # Select update values - if ($Field.Key -NotIn $PrimaryKey) { - if ($UpdateValues -eq $null) { $UpdateValues = "$($Field.Key) = $($Field.Value)" } - else { $UpdateValues += ", $($Field.Key) = $($Field.Value)" } + switch ($Vendor) { + "Oracle" { + # Define existence check + foreach($Key in $PrimaryKey) { + if ($PrimaryKeyCheck -eq $null) { $PrimaryKeyCheck = "$Key = $($Fields.$Key)" } + else { $PrimaryKeyCheck += " AND $Key = $($Fields.$Key)" } + } + $Check = [System.String]::Concat("MERGE INTO $Table USING dual ON (", $PrimaryKeyCheck, ")") + + # Loop through fields + foreach ($Field in $Fields.GetEnumerator()) { + # Select update values + if ($Field.Key -NotIn $PrimaryKey) { + if ($UpdateValues -eq $null) { $UpdateValues = "$($Field.Key) = $($Field.Value)" } + else { $UpdateValues += ", $($Field.Key) = $($Field.Value)" } + } + # Set insert fields + if ($InsertFields -eq $null) { $InsertFields = "$($Field.Key)" } + else { $InsertFields += ", $($Field.Key)" } + # Set insert values + if ($InsertValues -eq $null) { $InsertValues = "$($Field.Value)" } + else { $InsertValues += ", $($Field.Value)" } + } + + # Construct update query + $Update = [System.String]::Concat("WHEN MATCHED THEN UPDATE SET ", $UpdateValues) + # Construct insert query + $Insert = [System.String]::Concat("WHEN NOT MATCHED THEN INSERT (", $InsertFields, ") VALUES (", $InsertValues, ")") + + # Construct whole SQL query + $Query = [System.String]::Concat($Check, "`n", $Update, "`n", $Insert) + + # TODO } - # Set insert fields - if ($InsertFields -eq $null) { $InsertFields = "$($Field.Key)" } - else { $InsertFields += ", $($Field.Key)" } - # Set insert values - if ($InsertValues -eq $null) { $InsertValues = "$($Field.Value)" } - else { $InsertValues += ", $($Field.Value)" } - } + "SQLServer" { + # Define existence check + foreach($Key in $PrimaryKey) { + if ($PrimaryKeyCheck -eq $null) { $PrimaryKeyCheck = " WHERE $Key = $($Fields.$Key)" } + else { $PrimaryKeyCheck += " AND $Key = $($Fields.$Key)" } + } + $Check = [System.String]::Concat("IF EXISTS (SELECT COUNT(1) FROM " , $Table, $PrimaryKeyCheck, ")") - # Construct update query - $Update = "UPDATE $Table SET " + $UpdateValues + $PrimaryKeyCheck - # Construct insert query - $Insert = "INSERT INTO $Table ($InsertFields) VALUES ($InsertValues)" + # Loop through fields + foreach ($Field in $Fields.GetEnumerator()) { + # Select update values + if ($Field.Key -NotIn $PrimaryKey) { + if ($UpdateValues -eq $null) { $UpdateValues = "$($Field.Key) = $($Field.Value)" } + else { $UpdateValues += ", $($Field.Key) = $($Field.Value)" } + } + # Set insert fields + if ($InsertFields -eq $null) { $InsertFields = "$($Field.Key)" } + else { $InsertFields += ", $($Field.Key)" } + # Set insert values + if ($InsertValues -eq $null) { $InsertValues = "$($Field.Value)" } + else { $InsertValues += ", $($Field.Value)" } + } - # Construct whole SQL query - $Query = $Check + "`nBEGIN`n`t" + $Update + "`nEND`nELSE`nBEGIN`n`t" + $Insert + "`nEND" + # Construct update query + $Update = [System.String]::Concat("UPDATE ", $Table, " SET ", $UpdateValues, $PrimaryKeyCheck) + # Construct insert query + $Insert = [System.String]::Concat("INSERT INTO ", $Table, " (", $InsertFields, ") VALUES (", $InsertValues, ")") - # Check identity flag - if ($PSBoundParameters.ContainsKey["Identiy"] -eq $true) { - # Manage IDENTITY_INSERT - $EnableIdentityInsert = "SET IDENTITY_INSERT $Table ON" - $DisableIdentityInsert = "SET IDENTITY_INSERT $Table OFF" - $Query = $EnableIdentityInsert + "`n" + $Query + "`n" + $DisableIdentityInsert - } + # Construct whole SQL query + $Query = [System.String]::Concat($Check, "`nBEGIN`n`t", $Update, "`nEND`nELSE`nBEGIN`n`t", $Insert, "`nEND") + # Check identity flag + if ($PSBoundParameters.ContainsKey["Identity"] -eq $true) { + # Manage IDENTITY_INSERT + $EnableIdentityInsert = "SET IDENTITY_INSERT $Table ON" + $DisableIdentityInsert = "SET IDENTITY_INSERT $Table OFF" + $Query = [System.String]::Concat($EnableIdentityInsert, "`n", $Query, "`n", $DisableIdentityInsert) + } + } + default { + Write-Log -Type "ERROR" -Object "Unsupported database vendor $Vendor" + return $null + } + } # Return query Write-Log -Type "DEBUG" -Object $Query return $Query