diff --git a/CHANGELOG.md b/CHANGELOG.md index 447dd4b69..20733854d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- Changes to xSQLServerDatabase + - Added parameter to specify collation for a database to be different from server + collation ([issue #767](https://github.com/PowerShell/xSQLServer/issues/767)). + - Fixed unit tests for Get-TargetResource to ensure correctly testing return + values ([issue #849](https://github.com/PowerShell/xSQLServer/issues/849)) + ## 8.2.0.0 - Changes to xSQLServer diff --git a/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.psm1 b/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.psm1 index 045a136f0..c57be31e7 100644 --- a/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.psm1 +++ b/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.psm1 @@ -17,6 +17,10 @@ Import-Module -Name (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Pare .PARAMETER SQLInstanceName The name of the SQL instance to be configured. + + .PARAMETER Collation + The name of the SQL collation to use for the new database. + Defaults to server collation. #> function Get-TargetResource @@ -44,13 +48,19 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] - $SQLInstanceName + $SQLInstanceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation ) $sqlServerObject = Connect-SQL -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName if ($sqlServerObject) { + $sqlDatabaseCollation = $sqlServerObject.Collation Write-Verbose -Message 'Getting SQL Databases' # Check database exists $sqlDatabaseObject = $sqlServerObject.Databases[$Name] @@ -59,6 +69,7 @@ function Get-TargetResource { Write-Verbose -Message "SQL Database name $Name is present" $Ensure = 'Present' + $sqlDatabaseCollation = $sqlDatabaseObject.Collation } else { @@ -72,6 +83,7 @@ function Get-TargetResource Ensure = $Ensure SQLServer = $SQLServer SQLInstanceName = $SQLInstanceName + Collation = $sqlDatabaseCollation } $returnValue @@ -93,6 +105,10 @@ function Get-TargetResource .PARAMETER SQLInstanceName The name of the SQL instance to be configured. + + .PARAMETER Collation + The name of the SQL collation to use for the new database. + Defaults to server collation. #> function Set-TargetResource { @@ -118,31 +134,71 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] - $SQLInstanceName + $SQLInstanceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation ) $sqlServerObject = Connect-SQL -SQLServer $SQLServer -SQLInstanceName $SQLInstanceName if ($sqlServerObject) { + if ($Ensure -eq 'Present') { - try + if (-not $PSBoundParameters.ContainsKey('Collation')) + { + $Collation = $sqlServerObject.Collation + } + elseif ($Collation -notin $sqlServerObject.EnumCollations().Name) + { + throw New-TerminatingError -ErrorType InvalidCollationError ` + -FormatArgs @($SQLServer, $SQLInstanceName, $Name, $Collation) ` + -ErrorCategory InvalidOperation + } + + $sqlDatabaseObject = $sqlServerObject.Databases[$Name] + + if ($sqlDatabaseObject) { - $sqlDatabaseObjectToCreate = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Database -ArgumentList $sqlServerObject, $Name - if ($sqlDatabaseObjectToCreate) + try + { + Write-Verbose -Message "Updating the database $Name with specified settings." + $sqlDatabaseObject.Collation = $Collation + $sqlDatabaseObject.Alter() + New-VerboseMessage -Message "Updated Database $Name." + } + catch { - Write-Verbose -Message "Adding to SQL the database $Name" - $sqlDatabaseObjectToCreate.Create() - New-VerboseMessage -Message "Created Database $Name" + throw New-TerminatingError -ErrorType UpdateDatabaseSetError ` + -FormatArgs @($SQLServer, $SQLInstanceName, $Name) ` + -ErrorCategory InvalidOperation ` + -InnerException $_.Exception } } - catch + else { - throw New-TerminatingError -ErrorType CreateDatabaseSetError ` - -FormatArgs @($SQLServer, $SQLInstanceName, $Name) ` - -ErrorCategory InvalidOperation ` - -InnerException $_.Exception + try + { + $sqlDatabaseObjectToCreate = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Database -ArgumentList $sqlServerObject, $Name + if ($sqlDatabaseObjectToCreate) + { + Write-Verbose -Message "Adding to SQL the database $Name." + $sqlDatabaseObjectToCreate.Collation = $Collation + $sqlDatabaseObjectToCreate.Create() + New-VerboseMessage -Message "Created Database $Name." + } + } + catch + { + throw New-TerminatingError -ErrorType CreateDatabaseSetError ` + -FormatArgs @($SQLServer, $SQLInstanceName, $Name) ` + -ErrorCategory InvalidOperation ` + -InnerException $_.Exception + } } } else @@ -152,9 +208,9 @@ function Set-TargetResource $sqlDatabaseObjectToDrop = $sqlServerObject.Databases[$Name] if ($sqlDatabaseObjectToDrop) { - Write-Verbose -Message "Deleting to SQL the database $Name" + Write-Verbose -Message "Deleting to SQL the database $Name." $sqlDatabaseObjectToDrop.Drop() - New-VerboseMessage -Message "Dropped Database $Name" + New-VerboseMessage -Message "Dropped Database $Name." } } catch @@ -184,6 +240,10 @@ function Set-TargetResource .PARAMETER SQLInstanceName The name of the SQL instance to be configured. + + .PARAMETER Collation + The name of the SQL collation to use for the new database. + Defaults to server collation. #> function Test-TargetResource { @@ -210,7 +270,12 @@ function Test-TargetResource [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] - $SQLInstanceName + $SQLInstanceName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation ) Write-Verbose -Message "Checking if database named $Name is present or absent" @@ -218,6 +283,11 @@ function Test-TargetResource $getTargetResourceResult = Get-TargetResource @PSBoundParameters $isDatabaseInDesiredState = $true + if (-not $PSBoundParameters.ContainsKey('Collation')) + { + $Collation = $getTargetResourceResult.Collation + } + switch ($Ensure) { 'Absent' @@ -236,6 +306,11 @@ function Test-TargetResource New-VerboseMessage -Message "Ensure is set to Present. The database $Name should be created" $isDatabaseInDesiredState = $false } + elseif ($getTargetResourceResult.Collation -ne $Collation) + { + New-VerboseMessage -Message 'Database exist but has the wrong collation.' + $isDatabaseInDesiredState = $false + } } } diff --git a/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.schema.mof b/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.schema.mof index 442e02a02..67b6e6f29 100644 --- a/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.schema.mof +++ b/DSCResources/MSFT_xSQLServerDatabase/MSFT_xSQLServerDatabase.schema.mof @@ -5,4 +5,5 @@ class MSFT_xSQLServerDatabase : OMI_BaseResource [Write, Description("An enumerated value that describes if the database is added (Present) or dropped (Absent). Valid values are 'Present' or 'Absent'. Default Value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Key, Description("The host name of the SQL Server to be configured.")] String SQLServer; [Key, Description("The name of the SQL instance to be configured.")] String SQLInstanceName; + [Write, Description("The name of the SQL collation to use for the new database. Defaults to server collation.")] String Collation; }; diff --git a/Examples/Resources/xSQLServerDatabase/1-CreateDatabase.ps1 b/Examples/Resources/xSQLServerDatabase/1-CreateDatabase.ps1 index a5127fae4..0bd9e3c29 100644 --- a/Examples/Resources/xSQLServerDatabase/1-CreateDatabase.ps1 +++ b/Examples/Resources/xSQLServerDatabase/1-CreateDatabase.ps1 @@ -2,6 +2,9 @@ .EXAMPLE This example shows how to create a database with the database name equal to 'Contoso'. + + The second example shows how to create a database + with a different collation. #> Configuration Example { @@ -24,5 +27,14 @@ Configuration Example SQLInstanceName = 'DSC' Name = 'Contoso' } + + xSQLServerDatabase Create_Database_with_different_collation + { + Ensure = 'Present' + SQLServer = 'SQLServer' + SQLInstanceName = 'DSC' + Name = 'AdventureWorks' + Collation = 'SQL_Latin1_General_Pref_CP850_CI_AS' + } } } diff --git a/README.md b/README.md index 30accd12d..b85df8c02 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,8 @@ database, please read: * **`[String]` SQLServer** _(Key)_: The host name of the SQL Server to be configured. * **`[String]` SQLInstanceName** _(Key)_: The name of the SQL instance to be configured. * **`[String]` Name** _(Key)_: The name of database to be created or dropped. +* **`[String]` Collation** _(Write)_: The name of the SQL collation to use + for the new database. Defaults to server collation. * **`[String]` Ensure** _(Write)_: When set to 'Present', the database will be created. When set to 'Absent', the database will be dropped. { *Present* | Absent }. diff --git a/Tests/Unit/MSFT_xSQLServerDatabase.Tests.ps1 b/Tests/Unit/MSFT_xSQLServerDatabase.Tests.ps1 index 12b13ef21..2863ee185 100644 --- a/Tests/Unit/MSFT_xSQLServerDatabase.Tests.ps1 +++ b/Tests/Unit/MSFT_xSQLServerDatabase.Tests.ps1 @@ -39,8 +39,10 @@ try $mockSqlDatabaseName = 'AdventureWorks' $mockInvalidOperationForCreateMethod = $false $mockInvalidOperationForDropMethod = $false + $mockInvalidOperationForAlterMethod = $false $mockExpectedDatabaseNameToCreate = 'Contoso' $mockExpectedDatabaseNameToDrop = 'Sales' + $mockSqlDatabaseCollation = 'SQL_Latin1_General_CP1_CI_AS' # Default parameters that are used for the It-blocks $mockDefaultParameters = @{ @@ -56,10 +58,25 @@ try New-Object Object | Add-Member -MemberType NoteProperty -Name InstanceName -Value $mockSqlServerInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name ComputerNamePhysicalNetBIOS -Value $mockSqlServerName -PassThru | + Add-Member -MemberType NoteProperty -Name Collation -Value $mockSqlDatabaseCollation -PassThru | + Add-Member -MemberType ScriptMethod -Name EnumCollations -Value { + return @( + ( New-Object Object | + Add-Member -MemberType NoteProperty Name -Value $mockSqlDatabaseCollation -PassThru + ), + ( New-Object Object | + Add-Member -MemberType NoteProperty Name -Value 'SQL_Latin1_General_CP1_CS_AS' -PassThru + ), + ( New-Object Object | + Add-Member -MemberType NoteProperty Name -Value 'SQL_Latin1_General_Pref_CP850_CI_AS' -PassThru + ) + ) + } -PassThru -Force | Add-Member -MemberType ScriptProperty -Name Databases -Value { return @{ - $mockSqlDatabaseName = ( New-Object Object | + $mockSqlDatabaseName = ( New-Object Object | Add-Member -MemberType NoteProperty -Name Name -Value $mockSqlDatabaseName -PassThru | + Add-Member -MemberType NoteProperty -Name Collation -Value $mockSqlDatabaseCollation -PassThru | Add-Member -MemberType ScriptMethod -Name Drop -Value { if ($mockInvalidOperationForDropMethod) { @@ -71,6 +88,12 @@ try throw "Called mocked Drop() method without dropping the right database. Expected '{0}'. But was '{1}'." ` -f $mockExpectedDatabaseNameToDrop, $this.Name } + } -PassThru | + Add-Member -MemberType ScriptMethod -Name Alter -Value { + if ($mockInvalidOperationForAlterMethod) + { + throw 'Mock Alter Method was called with invalid operation.' + } } -PassThru ) } @@ -84,6 +107,7 @@ try ( New-Object Object | Add-Member -MemberType NoteProperty -Name Name -Value $mockSqlDatabaseName -PassThru | + Add-Member -MemberType NoteProperty -Name Collation -Value '' -PassThru | Add-Member -MemberType ScriptMethod -Name Create -Value { if ($mockInvalidOperationForCreateMethod) { @@ -107,46 +131,54 @@ try } Context 'When the system is not in the desired state' { - It 'Should return the state as absent' { - $testParameters = $mockDefaultParameters - $testParameters += @{ - Name = 'UnknownDatabase' - } + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'UnknownDatabase' + Collation = 'SQL_Latin1_General_CP1_CI_AS' + } + It 'Should return the state as absent' { $result = Get-TargetResource @testParameters $result.Ensure | Should Be 'Absent' } It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @testParameters $result.SQLServer | Should Be $testParameters.SQLServer $result.SQLInstanceName | Should Be $testParameters.SQLInstanceName $result.Name | Should Be $testParameters.Name + $result.Collation | Should Be $testParameters.Collation } + It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } } Context 'When the system is in the desired state for a database' { - It 'Should return the state as present' { - $testParameters = $mockDefaultParameters - $testParameters += @{ - Name = 'AdventureWorks' - } + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'AdventureWorks' + Collation = 'SQL_Latin1_General_CP1_CI_AS' + } + + It 'Should return the state as present' { $result = Get-TargetResource @testParameters $result.Ensure | Should Be 'Present' } It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @testParameters $result.SQLServer | Should Be $testParameters.SQLServer $result.SQLInstanceName | Should Be $testParameters.SQLInstanceName $result.Name | Should Be $testParameters.Name + $result.Collation | Should Be $testParameters.Collation } It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } } @@ -164,6 +196,19 @@ try $testParameters += @{ Name = 'UnknownDatabase' Ensure = 'Present' + Collation = 'SQL_Latin1_General_CP1_CS_AS' + } + + $result = Test-TargetResource @testParameters + $result | Should Be $false + } + + It 'Should return the state as false when desired database exists but has the incorrect collation' { + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'AdventureWorks' + Ensure = 'Present' + Collation = 'SQL_Latin1_General_CP1_CS_AS' } $result = Test-TargetResource @testParameters @@ -171,7 +216,7 @@ try } It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } } @@ -198,6 +243,19 @@ try $testParameters += @{ Name = 'AdventureWorks' Ensure = 'Present' + Collation = 'SQL_Latin1_General_CP1_CI_AS' + } + + $result = Test-TargetResource @testParameters + $result | Should Be $true + } + + It 'Should return the state as true when desired database exists and has the correct collation' { + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'AdventureWorks' + Ensure = 'Present' + Collation = 'SQL_Latin1_General_CP1_CI_AS' } $result = Test-TargetResource @testParameters @@ -205,7 +263,7 @@ try } It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } } @@ -251,8 +309,19 @@ try { Set-TargetResource @testParameters } | Should Not Throw } + It 'Should not throw when changing the database collation' { + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'Contoso' + Ensure = 'Present' + Collation = 'SQL_Latin1_General_CP1_CS_AS' + } + + { Set-TargetResource @testParameters } | Should Not Throw + } + It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } It 'Should call the mock function New-Object with TypeName equal to Microsoft.SqlServer.Management.Smo.Database' { @@ -282,6 +351,7 @@ try } $mockInvalidOperationForCreateMethod = $true + $mockInvalidOperationForAlterMethod = $true Context 'When the system is not in the desired state and Ensure is set to Present' { It 'Should throw the correct error when Create() method was called with invalid operation' { @@ -297,8 +367,21 @@ try { Set-TargetResource @testParameters } | Should Throw $throwInvalidOperation } + It 'Should throw the correct error when invalid collation is specified' { + $testParameters = $mockDefaultParameters + $testParameters += @{ + Name = 'Sales' + Ensure = 'Present' + Collation = 'InvalidCollation' + } + + $throwInvalidOperation = ("The specified collation '{3}' is not a valid collation for database {2} on {0}\{1}." -f $mockSqlServerName, $mockSqlServerInstanceName, $testParameters.Name, $testParameters.Collation) + + { Set-TargetResource @testParameters } | Should Throw $throwInvalidOperation + } + It 'Should call the mock function Connect-SQL' { - Assert-MockCalled Connect-SQL -Exactly -Times 1 -Scope Context + Assert-MockCalled Connect-SQL -Exactly -Times 2 -Scope Context } It 'Should call the mock function New-Object with TypeName equal to Microsoft.SqlServer.Management.Smo.Database' { @@ -316,6 +399,7 @@ try $testParameters += @{ Name = 'AdventureWorks' Ensure = 'Absent' + Collation = 'SQL_Latin1_General_CP1_CS_AS' } It 'Should throw the correct error when Drop() method was called with invalid operation' { diff --git a/en-US/xSQLServerHelper.strings.psd1 b/en-US/xSQLServerHelper.strings.psd1 index 2ae1a6e5a..58c7a27af 100644 --- a/en-US/xSQLServerHelper.strings.psd1 +++ b/en-US/xSQLServerHelper.strings.psd1 @@ -141,6 +141,8 @@ ConvertFrom-StringData @' FailedToSetOwnerDatabase = Failed to set owner named {0} of the database named {1} on {2}\\{3}. FailedToSetPermissionDatabase = Failed to set permission for login named {0} of the database named {1} on {2}\\{3}. FailedToEnumDatabasePermissions = Failed to get permission for login named {0} of the database named {1} on {2}\\{3}. + UpdateDatabaseSetError = Failed to update database {1} on {0}\\{1} with specified changes. + InvalidCollationError = The specified collation '{3}' is not a valid collation for database {2} on {0}\\{1}. # SQLServerRole EnumMemberNamesServerRoleGetError = Failed to enumerate members of the server role named {2} on {0}\\{1}.