diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e0d8d2..e2c55420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ + + + +# 0.18.0 + +* Fixed: Rivet doesn't use the CommandTimeout property in rivet.json configuration file. +* `Export-Migration` will now allow references to objects in databases that have been applied before it. +* The `DatabaseOrder` setting in the rivet.json file has been removed in favor of a new `Databases` property that should +be the ordered-list of databases to migrate. +* `Export-Migration` will now include extended properties on schemas, views, and view columns. + # 0.17.0 -## Changes +## Changes * When initializing a database, Rivet now runs the migrations found in the schema.ps1 file, which contains the baseline database schema upon which all migrations should be applied. You can use the `Checkpoint-Migration` function to create @@ -12,10 +23,10 @@ a baseline `schema.ps1` file for your database(s). * Updated `Checkpoint-Migration` function: - * The `schema.ps1` file generated from `Checkpoint-Migration` is saved to the Migrations directory of each database - that is being checkpointed. - * Only migrations that have been applied to the database will be exported to the `schema.ps1` file. - * Migrations that have been checkpointed will be removed from the Migrations directory. +* The `schema.ps1` file generated from `Checkpoint-Migration` is saved to the Migrations directory of each database +that is being checkpointed. +* Only migrations that have been applied to the database will be exported to the `schema.ps1` file. +* Migrations that have been checkpointed will be removed from the Migrations directory. # 0.15.0 @@ -114,7 +125,7 @@ order. See `help about_Rivet_Configuration` for more information. ## Enhancements * Created `Merge-Migration` function for creating cumulative, roll up migrations. - + # 0.7.0 @@ -151,7 +162,7 @@ for half a second. * Obsoleted the parameter sets of the `Remove-CheckConstraint`, `Remove-DefaulConstraint`, `Remove-ForeignKey`, `Remove-Index`, `Remove-PrimaryKey`, and `Remove-UniqueKey` operations that use an inferred constraint/index name. These operations now expect the name of the constraint/index to drop with the `Name` parameter. -* Improved object model so that customizing index/constraint names is easier. +* Improved object model so that customizing index/constraint names is easier. * Added `about_Rivet_Cookbook` help topic to showing how to customize index/constraint names. * Updated and improved the `about_Rivet_Plugins` help topic. * Obsoleted the `Enable-ForeignKey` and `Disable-ForeignKey` operations. Use the `Enable-Constraint` and @@ -167,8 +178,8 @@ multiple names, IDs, or file names). * Results from `Invoke-SqlScript` operations cause silent error when formatted as a table. * Path to rivet.json file not showing in an error message when using implicit path. - - + + # 0.5.1 ## Enhancements @@ -181,8 +192,8 @@ all operations. ## Bug Fixes * Get-Migration fails when run from Convert-Migration: it doesn't know the path to use to load migrations from. - - + + # 0.5.0 ## Enhancements @@ -203,7 +214,7 @@ output replaces the old Write-Host output). * NOCHECK parameter has been added to `Add-ForeignKey` and `Add-CheckConstraint` operations * `Disable-CheckConstraint` and `Enable-CheckConstraint` functions have been added. * `Disable-ForeignKey` and `Enable-ForeignKey` functions have been added. - + ## Bug Fixes * Convert-Migration.ps1 generates incorrect SQL if a migration removes then re-adds a column. @@ -229,7 +240,7 @@ output replaces the old Write-Host output). * `Get-RivetConfig` is now a publicly exposed function. Use this method to parse a Rivet JSON configuration file. It returns a `Rivet.Configuration.Configuration` object. - + # 0.3.0 ## Enhancements @@ -263,4 +274,4 @@ option. See `about_Rivet_Configuration` for more information. * Rivet now updates its internal objects using migrations (i.e. it is now self-migrating). It uses (and reserves) migration IDs below 01000000000000. If you have migrations with these IDs, you'll need to give them new IDs and update IDs in any rivet.Migrations table that uses that ID. -* Migration name maximum length increased to 241 characters (the theoretical maximum allowed by Windows). \ No newline at end of file +* Migration name maximum length increased to 241 characters (the theoretical maximum allowed by Windows). diff --git a/Rivet/Functions/Checkpoint-Migration.ps1 b/Rivet/Functions/Checkpoint-Migration.ps1 index 2a34922b..779db19b 100644 --- a/Rivet/Functions/Checkpoint-Migration.ps1 +++ b/Rivet/Functions/Checkpoint-Migration.ps1 @@ -66,7 +66,7 @@ function Checkpoint-Migration } Write-Debug "Checkpoint-Migration: Exporting migration on database $($databaseItem.Name)" - $migration = Export-Migration -SqlServerName $settings.SqlServerName -Database $databaseItem.Name + $migration = Export-Migration -SqlServerName $settings.SqlServerName -Database $databaseItem.Name -ConfigFilePath $ConfigFilePath $migration = $migration -join [Environment]::NewLine Set-Content -Path $OutputPath -Value $migration diff --git a/Rivet/Functions/Convert-FileInfoToMigration.ps1 b/Rivet/Functions/Convert-FileInfoToMigration.ps1 index 91d42a66..b22bef11 100644 --- a/Rivet/Functions/Convert-FileInfoToMigration.ps1 +++ b/Rivet/Functions/Convert-FileInfoToMigration.ps1 @@ -58,6 +58,9 @@ function Convert-FileInfoToMigration continue } + # Set CommandTimeout on operation to value from Rivet configuration. + $operationItem.CommandTimeout = $Configuration.CommandTimeout + $pluginParameter = @{ Migration = $m ; Operation = $_ } [Rivet.Operations.Operation[]]$operations = & { diff --git a/Rivet/Functions/Export-Migration.ps1 b/Rivet/Functions/Export-Migration.ps1 index 0d49dca4..38f329ea 100644 --- a/Rivet/Functions/Export-Migration.ps1 +++ b/Rivet/Functions/Export-Migration.ps1 @@ -8,8 +8,6 @@ function Export-Migration .DESCRIPTION The `Export-Migration` function exports database objects, schemas, and data types as a Rivet migration. By default, it exports *all* non-system, non-Rivet objects, data types, and schemas. You can filter specific objects by passing their full name to the `Include` parameter. Wildcards are supported. Objects are matched on their schema *and* name. - Extended properties are *not* exported, except table and column descriptions. - .EXAMPLE Export-Migration -SqlServerName 'some\instance' -Database 'database' @@ -17,38 +15,40 @@ function Export-Migration #> [CmdletBinding()] param( - [Parameter(Mandatory)] - [string] # The connection string for the database to connect to. - $SqlServerName, - [Parameter(Mandatory)] - [string] - # The database to connect to. - $Database, + [String] $SqlServerName, - [string[]] + # The database to connect to. + [Parameter(Mandatory)] + [String] $Database, + # The names of the objects to export. Must include the schema if exporting a specific object. Wildcards supported. # # The default behavior is to export all non-system objects. - $Include, + [String[]] $Include, + + # The name of the environment whose settings to return. If not provided, uses the default settings. + [String] $Environment, - [string[]] # The names of any objects *not* to export. Matches the object name *and* its schema name, i.e. `schema.name`. Wildcards supported. - $Exclude, + [String[]] $Exclude, - [string[]] - [ValidateSet('CheckConstraint','DataType','DefaultConstraint','ForeignKey','Function','Index','PrimaryKey','Schema','StoredProcedure','Synonym','Table','Trigger','UniqueKey','View','XmlSchema')] # Any object types to exclude. - $ExcludeType, + [ValidateSet('CheckConstraint','DataType','DefaultConstraint','ForeignKey','Function','Index','PrimaryKey','Schema','StoredProcedure','Synonym','Table','Trigger','UniqueKey','View','XmlSchema')] + [String[]] $ExcludeType, - [Switch] - $NoProgress + # The path to the Rivet configuration file to load. Defaults to `rivet.json` in the current directory. + [String] $ConfigFilePath, + + [Switch] $NoProgress ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState + [Rivet.Configuration.Configuration]$settings = Get-RivetConfig -Path $ConfigFilePath -Environment $Environment + $pops = New-Object 'Collections.Generic.Stack[string]' $popsHash = @{} $exportedObjects = @{ } @@ -971,9 +971,17 @@ where if( $externalDependencies.ContainsKey($object.object_id) ) { - Write-Warning -Message ('Unable to export {0} {1}: it depends on external object {2}.' -f $object.type_desc,$object.full_name,$externalDependencies[$object.object_id]) - $exportedObjects[$object.object_id] = $true - continue + $indexOfReferencedDatabase = [array]::IndexOf($settings.Databases.Name, $externalDependencies[$object.object_id].DatabaseName) + $indexOfCurrentDatabase = [array]::IndexOf($settings.Databases.Name, $Database) + + # If the external depenedency's database does not get applied BEFORE the current database, do not allow + # references to the external dependency. + if( ($indexOfReferencedDatabase -gt $indexOfCurrentDatabase) -or ($indexOfReferencedDatabase -lt 0) -or ($indexOfCurrentDatabase -lt 0) ) + { + Write-Warning -Message ('Unable to export {0} {1}: it depends on external object {2}.' -f $object.type_desc,$object.full_name,$externalDependencies[$object.object_id].ExternalName) + $exportedObjects[$object.object_id] = $true + continue + } } switch ($object.type_desc) @@ -1163,12 +1171,18 @@ where -- SCHEMAS select sys.schemas.name, - sys.sysusers.name as owner + sys.sysusers.name as owner, + sys.extended_properties.value as description from sys.schemas join sys.sysusers - on sys.schemas.principal_id = sys.sysusers.uid' + on sys.schemas.principal_id = sys.sysusers.uid + left join + sys.extended_properties + on sys.extended_properties.class = 3 + and sys.extended_properties.major_id = sys.schemas.schema_id + and sys.extended_properties.name = ''MS_Description''' function Export-Schema { param( @@ -1187,9 +1201,14 @@ from { return } + $description = $schema.description + if( $description ) + { + $description = ' -Description ''{0}''' -f ($description -replace '''','''''') + } Write-ExportingMessage -Schema $Object.schema_name -Type Schema - ' Add-Schema -Name ''{0}'' -Owner ''{1}''' -f $schema.name,$schema.owner + ' Add-Schema -Name ''{0}'' -Owner ''{1}''{2}' -f $schema.name,$schema.owner, $description $exportedSchemas[$schema.name] = $true Push-PopOperation ('Remove-Schema -Name ''{0}''' -f $schema.name) } @@ -1534,8 +1553,22 @@ where Write-ExportingMessage -Schema $Object.schema_name -Name $Object.name -Type View if( $view -match $createPreambleRegex ) { + $description = $Object.description + if( $description ) + { + $description = ' -Description ''{0}''' -f ($description -replace '''','''''') + } + $view = $view -replace $createPreambleRegex,'' - ' Add-View{0} -Name ''{1}'' -Definition @''{2}{3}{2}''@' -f $schema,$Object.name,[Environment]::NewLine,$view + ' Add-View{0} -Name ''{1}''{2} -Definition @''{3}{4}{3}''@' -f $schema,$Object.name,$description,[Environment]::NewLine,$view + + # Get view's columns that have extended properties + $viewColumns = Invoke-Query -Query $columnsQuery | Where-Object { $_.object_id -eq $Object.object_id -and $_.description } + foreach( $column in $viewColumns ) + { + $colDescription = ' -Description ''{0}''' -f ($column.description -replace '''','''''') + ' Add-ExtendedProperty -SchemaName ''{0}'' -ViewName ''{1}'' -ColumnName ''{2}'' -Value {3}' -f $Object.schema_name,$Object.object_name,$column.column_name,$colDescription + } } else { @@ -1796,7 +1829,7 @@ where if( $objectTypes -contains 'SQL_TRIGGER' ) { $triggers = Invoke-Query -Query $triggersQuery - $triggers | ForEach-Object { $triggersByID[$_.object_id] = $_ } + $triggers | ForEach-Object { $triggersByID[$_.object_id] = $_ } $triggers | Group-Object -Property 'parent_id' | ForEach-Object { $triggersByTable[[int]$_.Name] = $_.Group } } @@ -1806,7 +1839,7 @@ where $uniqueKeys = Invoke-Query -Query $uniqueKeysQuery $uniqueKeys | ForEach-Object { $uniqueKeysByID[$_.object_id] = $_ } $uniqueKeys | Group-Object -Property 'parent_object_id' | ForEach-Object { $uniqueKeysByTable[[int]$_.Name] = $_.Group } - + # UNIQUE KEY COLUMNS $uniqueKeyColumns = Invoke-Query -Query $uniqueKeysColumnsQuery $uniqueKeyColumns | Group-Object -Property 'object_id' | ForEach-Object { $uniqueKeyColumnsByObjectID[[int]$_.Name] = $_.Group } @@ -1862,7 +1895,7 @@ where if( $row.referenced_server_name -or ($row.referenced_database_name -ne $null -and $row.referenced_database_name -ne $Database) ) { - $externalDependencies[$row.referencing_id] = $externalName + $externalDependencies[$row.referencing_id] = @{ ExternalName = $externalName; DatabaseName = $row.referenced_database_name } } else { @@ -1882,21 +1915,21 @@ where $schemas $indexes $dataTypes - } | - Measure-Object | + } | + Measure-Object | Select-Object -ExpandProperty 'Count' if( $writeProgress ) { - Write-Progress -Activity $activity + Write-Progress -Activity $activity } - $timer | + $timer | Add-Member -Name 'ExportCount' -Value 0 -MemberType NoteProperty -PassThru | Add-Member -MemberType NoteProperty -Name 'Activity' -Value $activity -PassThru | Add-Member -MemberType NoteProperty -Name 'CurrentOperation' -Value '' -PassThru | Add-Member -MemberType NoteProperty -Name 'TotalCount' -Value $totalOperationCount - + if( $writeProgress ) { # Write-Progress is *expensive*. Only do it if the user is interactive and only every 1/10th of a second. diff --git a/Rivet/Functions/Get-RivetConfig.ps1 b/Rivet/Functions/Get-RivetConfig.ps1 index aff98c75..4b696a85 100644 --- a/Rivet/Functions/Get-RivetConfig.ps1 +++ b/Rivet/Functions/Get-RivetConfig.ps1 @@ -270,7 +270,7 @@ function Get-RivetConfig $targetDatabases = @{ } } - $order = Get-ConfigProperty -Name 'DatabaseOrder' -AsArray + $order = Get-ConfigProperty -Name 'Databases' -AsArray $pluginModules = Get-ConfigProperty -Name 'PluginModules' -AsArray [Rivet.Configuration.Configuration]$configuration = @@ -285,22 +285,30 @@ function Get-RivetConfig # Get user-specified databases first if( $Database ) { - $Database | + return $Database | Add-Member -MemberType ScriptProperty -Name Name -Value { $this } -PassThru | Add-Member -MemberType ScriptProperty -Name FullName -Value { Join-Path -Path $configuration.DatabasesRoot -ChildPath $this.Name } -PassThru } - else - { - # Then get all of them in the order requested - if( $order ) + + # Default alphabetical order + if (-not $order) + { + return Get-ChildItem -Path $configuration.DatabasesRoot -Directory + } + + # User specified order + foreach( $dbName in $order ) + { + $dbPath = Join-Path -Path $configuration.DatabasesRoot -ChildPath $dbName + if (-not (Test-Path -Path $dbPath -PathType Container)) { - foreach( $dbName in $order ) + if (-not [wildcardpattern]::ContainsWildcardCharacters($dbName)) { - Get-ChildItem -Path $configuration.DatabasesRoot -Filter $dbName -Directory + Write-ValidationError "database named ""$($dbName)"" at ""$($dbPath)"" does not exist" } + continue } - - Get-ChildItem -Path $configuration.DatabasesRoot -Exclude $order -Directory + Get-Item -Path $dbPath } } | Select-Object -Property Name,FullName -Unique | diff --git a/Rivet/Functions/Operations/Add-Description.ps1 b/Rivet/Functions/Operations/Add-Description.ps1 index 619a8244..1cd54560 100644 --- a/Rivet/Functions/Operations/Add-Description.ps1 +++ b/Rivet/Functions/Operations/Add-Description.ps1 @@ -3,7 +3,7 @@ function Add-Description { <# .SYNOPSIS - Adds the `MS_Description` extended property to a table or column. + Adds the `MS_Description` extended property to schemas, tables, columns, views, and view columns. .DESCRIPTION The `sys.sp_addextendedproperty` stored procedure is used to set a table/column's description (i.e. the `MS_Description` extended property), but the syntax is weird. This function hides that weirdness from you. You're welcome. @@ -22,33 +22,62 @@ function Add-Description Add-Description -Description 'Whoseit's whatsits table.' -TableName WhoseitsWhatsits -ForTable PowerShell v2.0 doesn't parse the parameters correctly when setting a table name, so you have to explicitly tell it what to do. Upgrade to PowerShell 3! + + .EXAMPLE + Add-Description -Description 'This is an extended property on a schema' -SchemaName 'test' + + Adds a description (i.e. the `MS_Description` extended property) on the `test` schema. + + .EXAMPLE + Add-Description -Description 'This is an extended property on a view' -SchemaName 'test' -ViewName 'testVw' + + Adds a description (i.e. the `MS_Description` extended property) on the `testVw` view. + + .EXAMPLE + Add-Description -Description 'This is an extended property on a view column' -SchemaName 'test' -ViewName 'testVw' -ColumnName 'ID' + + Adds a description (i.e. the `MS_Description` extended property) on the `ID` column in the 'testVw' view. #> [CmdletBinding()] param( - [Parameter(Mandatory=$true,Position=0)] - [string] # The value for the MS_Description extended property. - $Description, + [Parameter(Mandatory, Position=0)] + [String] $Description, - [Alias('Schema')] - [string] # The schema. Defaults to `dbo`. - $SchemaName = 'dbo', + [Parameter(ParameterSetName='ForSchema')] + [Parameter(ParameterSetName='ForTable')] + [Parameter(ParameterSetName='ForView')] + [Parameter(ParameterSetName='ForColumn')] + [Alias('Schema')] + [String] $SchemaName = 'dbo', - [Parameter(Mandatory=$true)] - [Alias('Table')] - [string] # The name of the table where the extended property is getting set. - $TableName, + [Parameter(Mandatory, ParameterSetName='ForTable')] + [Parameter(Mandatory, ParameterSetName='ForColumn')] + [Alias('Table')] + [String] $TableName, + + # The name of the view where the extended property is getting set. + [Parameter(Mandatory, ParameterSetName='ForView')] + [Alias('View')] + [String] $ViewName, - [Parameter(ParameterSetName='ForColumn')] - [Alias('Column')] - [string] # The name of the column where the extended property is getting set. - $ColumnName + [Parameter(Mandatory, ParameterSetName='ForColumn')] + [Alias('Column')] + [String] $ColumnName ) $optionalArgs = @{ } + if( $TableName ) + { + $optionalArgs.TableName = $TableName + } + if( $ViewName ) + { + $optionalArgs.ViewName = $ViewName + } if( $ColumnName ) { $optionalArgs.ColumnName = $ColumnName @@ -57,6 +86,5 @@ function Add-Description Add-ExtendedProperty -Name ([Rivet.Operations.ExtendedPropertyOperation]::DescriptionPropertyName) ` -Value $Description ` -SchemaName $SchemaName ` - -TableName $TableName ` @optionalArgs } \ No newline at end of file diff --git a/Rivet/Functions/Operations/Add-Schema.ps1 b/Rivet/Functions/Operations/Add-Schema.ps1 index 7753643f..1ab23930 100644 --- a/Rivet/Functions/Operations/Add-Schema.ps1 +++ b/Rivet/Functions/Operations/Add-Schema.ps1 @@ -6,28 +6,45 @@ function Add-Schema Creates a new schema. .DESCRIPTION - The `Add-Schema` operation creates a new schema in a database. It does so in an idempotent way, i.e. it only creates the schema if it doesn't exist. + The `Add-Schema` operation creates a new schema in a database. It does so in an idempotent way, i.e. it only + creates the schema if it doesn't exist. If -Description is provided an extended property named 'MS_Description' + will be added to the schema with the description as the value. .EXAMPLE Add-Schema -Name 'rivetexample' Creates the `rivetexample` schema. + + .EXAMPLE + Add-Schema -Name 'rivetTest' -Description 'This is an extended property' + + Creates the `rivetTest` schema with the `MS_Description` extended property. #> [CmdletBinding()] param( - [Parameter(Mandatory=$true)] - [Alias('SchemaName')] - [string] # The name of the schema. - $Name, + [Parameter(Mandatory)] + [Alias('SchemaName')] + [String] $Name, - [Alias('Authorization')] - [string] # The owner of the schema. - $Owner + [Alias('Authorization')] + [String] $Owner, + + # A description of the schema. + [String] $Description ) Set-StrictMode -Version 'Latest' + + $schemaOp = New-Object 'Rivet.Operations.AddSchemaOperation' $Name, $Owner + + if( $Description ) + { + $schemaDescriptionOp = Add-Description -SchemaName $Name -Description $Description + $schemaOp.ChildOperations.Add($schemaDescriptionOp) + } - New-Object 'Rivet.Operations.AddSchemaOperation' $Name, $Owner + $schemaOp | Write-Output + $schemaOp.ChildOperations | Write-Output } \ No newline at end of file diff --git a/Rivet/Functions/Operations/Add-View.ps1 b/Rivet/Functions/Operations/Add-View.ps1 index 621e455c..138cc24c 100644 --- a/Rivet/Functions/Operations/Add-View.ps1 +++ b/Rivet/Functions/Operations/Add-View.ps1 @@ -5,32 +5,47 @@ Creates a new view. .DESCRIPTION - Creates a new view. + Creates a new view. If -Description is provided an extended property named 'MS_Description' will be added to the + schema with the description as the value. .EXAMPLE Add-View -SchemaName 'rivet' 'ReadMigrations' 'AS select * from rivet.Migrations' Creates a view to read all the migrations from Rivet's Migrations table. Don't do this in real life. + + .EXAMPLE + Add-View -Name 'rivetVw' -Description 'This is an extended property' + + Creates the `rivetVw` view with the `MS_Description` extended property. #> [CmdletBinding()] param( - [Parameter(Mandatory=$true,Position=0)] - [string] # The name of the view. - $Name, + [Parameter(Mandatory, Position=0)] + [String] $Name, - [Parameter()] - [string] # The schema name of the view. Defaults to `dbo`. - $SchemaName = 'dbo', + [Parameter()] + [String] $SchemaName = 'dbo', - [Parameter(Mandatory=$true,Position=1)] - [string] # The definition of the view. Everything after the `create view [schema].[name]` clause. - $Definition + [Parameter(Mandatory, Position=1)] + [String] $Definition, + + # A description of the view. + [String] $Description ) Set-StrictMode -Version 'Latest' - New-Object 'Rivet.Operations.AddViewOperation' $SchemaName,$Name,$Definition + $viewOp = New-Object 'Rivet.Operations.AddViewOperation' $SchemaName,$Name,$Definition + + if( $Description ) + { + $viewDescriptionOp = Add-Description -SchemaName $SchemaName -ViewName $Name -Description $Description + $viewOp.ChildOperations.Add($viewDescriptionOp) + } + + $viewOp | Write-Output + $viewOp.ChildOperations | Write-Output } \ No newline at end of file diff --git a/Rivet/Rivet.psd1 b/Rivet/Rivet.psd1 index e9fb0834..ae1fc80b 100644 --- a/Rivet/Rivet.psd1 +++ b/Rivet/Rivet.psd1 @@ -11,7 +11,7 @@ RootModule = 'Rivet.psm1' # Version number of this module. - ModuleVersion = '0.17.0' + ModuleVersion = '0.18.0' # ID used to uniquely identify this module GUID = '8af34b47-259b-4630-a945-75d38c33b94d' @@ -63,7 +63,7 @@ Rivet is a database migration/change management/versioning tool inspired by Ruby #TypesToProcess = '' # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @( + FormatsToProcess = @( 'Formats\RivetMigrationResult-GroupingFormat.format.ps1xml', 'Formats\RivetOperationResult-GroupingFormat.format.ps1xml', 'Formats\Rivet.Migration.format.ps1xml', diff --git a/Rivet/en-US/about_Rivet_Configuration.help.txt b/Rivet/en-US/about_Rivet_Configuration.help.txt index e559f82d..7c0c2379 100644 --- a/Rivet/en-US/about_Rivet_Configuration.help.txt +++ b/Rivet/en-US/about_Rivet_Configuration.help.txt @@ -57,7 +57,7 @@ LONG DESCRIPTION The amount of time, in seconds, to wait for a database connection to open. The default is 15 seconds. - ### DatabaseOrder + ### Databases A list of database names in the order in which Rivet should apply migrations. The default is alphabetical order. If a database is listed here but doesn't exist on the @@ -166,7 +166,7 @@ LONG DESCRIPTION "ConnectionTimeout": 5, "CommandTimeout": 300, "IgnoreDatabases": [ "Shared" ], - "DatabaseOrder": [ "Xanadu", "Q" ] + "Databases": [ "Xanadu", "Q" ] } This example demonstrates how to use all the configuration options. This diff --git a/Test/Export-Migration.Tests.ps1 b/Test/Export-Migration.Tests.ps1 index 87aef838..18c5c082 100644 --- a/Test/Export-Migration.Tests.ps1 +++ b/Test/Export-Migration.Tests.ps1 @@ -16,8 +16,7 @@ function GivenDatabase { param( [Parameter(Mandatory)] - [string[]] - $Name + [String[]] $Name ) $script:databases += $Name @@ -27,8 +26,7 @@ function GivenMigrationContent { param( [Parameter(Mandatory)] - [string] - $Content + [String] $Content ) $script:originalMigration = $Content @@ -44,12 +42,10 @@ function ThenNoErrors function ThenMigration { param( - [Switch] - $Not, + [Switch] $Not, [Parameter(Mandatory)] - [string] - $HasContent + [String] $HasContent ) It ('should export operations') { @@ -68,29 +64,35 @@ function WhenExporting { [CmdletBinding()] param( - [string[]] - $Include, + [String[]] $Include, - [Switch] - $SkipVerification, + [Switch] $SkipVerification, - [string[]] - $ExcludeType, + [String[]] $ExcludeType, - [string[]] - $Exclude + [String[]] $Exclude, + + [String] $Database ) - Start-RivetTest + if( -not $Database ) + { + $Database = $RTDatabaseName + } + + Start-RivetTest -DatabaseName $Database try { + $testDirectory = Get-ChildItem -Path $TestDrive.FullName | Sort-Object -Property LastWriteTime | Select-Object -Last 1 + $configFilePath = Join-Path -Path $testDirectory.FullName -ChildPath 'rivet.json' + $migrationPath = '' if( $originalMigration ) { - $migrationPath = $originalMigration | New-TestMigration -Name 'ExportMigration' + $migrationPath = $originalMigration | New-TestMigration -Name 'ExportMigration' -DatabaseName $Database -ConfigFilePath $configFilePath } - Invoke-RTRivet -Push + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $Database $optionalParams = @{} if( $PSBoundParameters.ContainsKey('Include') ) { @@ -109,7 +111,7 @@ function WhenExporting $Global:Error.Clear() - $script:migration = Export-Migration -SqlServerName $RTServer -Database $RTDatabaseName @optionalParams + $script:migration = Export-Migration -SqlServerName $RTServer -Database $Database -ConfigFilePath $configFilePath @optionalParams Write-Debug -Message ($migration -join [Environment]::NewLine) if( -not $SkipVerification ) @@ -126,9 +128,8 @@ function WhenExporting } finally { - Stop-RivetTest + Stop-RivetTest -DatabaseName $Database } - } Describe 'Export-Migration.when exporting a table' { @@ -1236,3 +1237,211 @@ function Pop-Migration ThenMigration -HasContent 'Add-Index -TableName ''Table1'' -ColumnName ''AnotherID''' } +Describe 'Export-Migration.when schema has an extended property' { + Init + GivenMigrationContent @' + function Push-Migration + { + Add-Schema 'snap' + Add-ExtendedProperty -Name 'MS_Description' -Value 'This is the MS Description for the schema snap' -SchemaName 'snap' + Add-Table -Schema 'snap' -Name 'SnapTable' -Column { + int 'ID' -NotNull + } + } + + function Pop-Migration + { + Remove-Table -Schema 'snap' -Name 'SnapTable' + Remove-Schema -Name 'snap' + } +'@ + + WhenExporting + ThenMigration -HasContent 'Add-Schema -Name ''snap'' -Owner ''dbo'' -Description ''This is the MS Description for the schema snap''' + ThenMigration -HasContent 'Add-Table -SchemaName ''snap'' -Name ''SnapTable''' +} + +Describe 'Export-Migration.when view has an extended property' { + Init + GivenMigrationContent @' + function Push-Migration + { + Add-Schema 'crackle' + Add-View -SchemaName 'crackle' -Name 'CrackleView' -Definition 'as select 1 as one' + Add-ExtendedProperty -Name 'MS_Description' -SchemaName 'crackle' -ViewName 'CrackleView' -Value 'This is the MS Description for the view CrackleView' + } + + function Pop-Migration + { + Remove-View -SchemaName 'crackle' -Name 'CrackleView' + Remove-Schema 'crackle' + } +'@ + + WhenExporting + ThenMigration -HasContent 'Add-Schema -Name ''crackle''' + ThenMigration -HasContent 'Add-View -SchemaName ''crackle'' -Name ''CrackleView''' + ThenMigration -HasContent 'Add-View -SchemaName ''crackle'' -Name ''CrackleView'' -Description ''This is the MS Description for the view CrackleView''' +} + +Describe 'Export-Migration.when view column has an extended property' { + Init + GivenMigrationContent @' + function Push-Migration + { + Add-Schema 'pop' + Add-Table -Schema 'pop' -Name 'PopTable' -Column { + int 'ID' -NotNull + } + Add-View -Name 'PopView' -SchemaName 'pop' -Definition 'as select * from PopTable' + Add-ExtendedProperty -Name 'MS_Description' -SchemaName 'pop' -ViewName 'PopView' -ColumnName 'ID' -Value 'This is the MS Description for column ID in the view PopView' + } + + function Pop-Migration + { + Remove-Table -SchemaName 'pop' -Name 'PopTable' + Remove-View -SchemaName 'pop' -Name 'PopView' + Remove-Schema 'pop' + } +'@ + + WhenExporting + ThenMigration -HasContent 'Add-Schema -Name ''pop''' + ThenMigration -HasContent 'Add-Table -SchemaName ''pop'' -Name ''PopTable''' + ThenMigration -HasContent 'Add-View -SchemaName ''pop'' -Name ''PopView''' + ThenMigration -HasContent 'Add-ExtendedProperty -SchemaName ''pop'' -ViewName ''PopView'' -ColumnName ''ID'' -Value -Description ''This is the MS Description for column ID in the view PopView''' +} + +Describe 'Export-Migration.when an object references another database that was applied before it' { + Init + $Databases = @($RTDatabaseName, $RTDatabase2Name) + + # Has Database Order + Start-RivetTest -PhysicalDatabase $Databases -ConfigurationDatabase $Databases + $testDirectory = Get-ChildItem -Path $TestDrive.FullName + $configFilePath = Join-Path -Path $testDirectory.FullName -ChildPath 'rivet.json' + + $db1Migration = @' + function Push-Migration + { + Add-Table -Name 'Table1DB1' -Column { + int 'ID' -NotNull + } + } + + function Pop-Migration + { + Remove-Table -Name 'Table1DB1' + } +'@ + $db1Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabaseName -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabaseName + + $db2Migration = @" + function Push-Migration + { + Add-View -Name 'ViewOfTable1DB1' -Definition 'as select * from $($RTDatabaseName).dbo.Table1DB1' + } + + function Pop-Migration + { + Remove-View -Name 'ViewOfTable1DB1' + } +"@ + $db2Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabase2Name -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabase2Name + + $script:migration = Export-Migration -SqlServerName $RTServer -Database $RTDatabase2Name -ConfigFilePath $configFilePath + ThenMigration -HasContent 'Add-View -Name ''ViewOfTable1DB1''' + Stop-RivetTest -DatabaseName $Databases + ThenNoErrors +} + +Describe 'Export-Migration.when an object references another database that was applied after it' { + Init + $Databases = @($RTDatabase2Name, $RTDatabaseName) + + # Has Database Order + Start-RivetTest -PhysicalDatabase $Databases -ConfigurationDatabase $Databases + $testDirectory = Get-ChildItem -Path $TestDrive.FullName + $configFilePath = Join-Path -Path $testDirectory.FullName -ChildPath 'rivet.json' + + $db1Migration = @' + function Push-Migration + { + Add-Table -Name 'Table1DB1' -Column { + int 'ID' -NotNull + } + } + + function Pop-Migration + { + Remove-Table -Name 'Table1DB1' + } +'@ + $db1Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabaseName -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabaseName + + $db2Migration = @" + function Push-Migration + { + Add-View -Name 'ViewOfTable1DB1' -Definition 'as select * from $($RTDatabaseName).dbo.Table1DB1' + } + + function Pop-Migration + { + Remove-View -Name 'ViewOfTable1DB1' + } +"@ + $db2Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabase2Name -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabase2Name + + $script:migration = Export-Migration -SqlServerName $RTServer -Database $RTDatabase2Name -ConfigFilePath $configFilePath + ThenMigration -Not -HasContent 'Add-View -Name ''ViewOfTable1DB1''' + Stop-RivetTest -DatabaseName $Databases +} + +Describe 'Export-Migration.when an object references another database and no ConfigurationDatabase is specified' { + Init + $Databases = @($RTDatabase2Name, $RTDatabaseName) + + # No Database Order + Start-RivetTest -PhysicalDatabase $Databases + $testDirectory = Get-ChildItem -Path $TestDrive.FullName + $configFilePath = Join-Path -Path $testDirectory.FullName -ChildPath 'rivet.json' + + $db1Migration = @' + function Push-Migration + { + Add-Table -Name 'Table1DB1' -Column { + int 'ID' -NotNull + } + } + + function Pop-Migration + { + Remove-Table -Name 'Table1DB1' + } +'@ + $db1Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabaseName -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabaseName + + $db2Migration = @" + function Push-Migration + { + Add-View -Name 'ViewOfTable1DB1' -Definition 'as select * from $($RTDatabaseName).dbo.Table1DB1' + } + + function Pop-Migration + { + Remove-View -Name 'ViewOfTable1DB1' + } +"@ + $db2Migration | New-TestMigration -Name 'ExportMigration' -DatabaseName $RTDatabase2Name -ConfigFilePath $configFilePath + Invoke-RTRivet -Push -ConfigFilePath $configFilePath -Database $RTDatabase2Name + + $script:migration = Export-Migration -SqlServerName $RTServer -Database $RTDatabase2Name -ConfigFilePath $configFilePath + ThenMigration -HasContent 'Add-View -Name ''ViewOfTable1DB1''' + Stop-RivetTest -DatabaseName $Databases + ThenNoErrors +} \ No newline at end of file diff --git a/Test/Get-Migration.Tests.ps1 b/Test/Get-Migration.Tests.ps1 index 7bd885b3..09f6ae2b 100644 --- a/Test/Get-Migration.Tests.ps1 +++ b/Test/Get-Migration.Tests.ps1 @@ -338,6 +338,45 @@ Describe 'Get-Migration' { 'ShouldGetAMigrationByBaseNameWithWildcard' | Should -Be $result.Name $m.BaseName | Should -Be $result.FullName } + + It 'should set timeout duration to default 30 seconds on Rivet operations when CommandTimeout isn''t specified in the Rivet configuration' { + @' + function Push-Migration + { + Invoke-Ddl 'select 1' + } + function Pop-Migration + { + Invoke-Ddl 'select 1' + } +'@ | New-TestMigration -Name 'SetCommandTimeout' + + $m = Get-Migration -ConfigFilePath $RTConfigFilePath + $Global:Error.Count | Should -Be 0 + $m | Should -BeOfType ([Rivet.Migration]) + $m.PopOperations[0].CommandTimeout | Should -Be 30 + $m.PushOperations[0].CommandTimeout | Should -Be 30 + } + + It 'should set timeout duration on Rivet operations when CommandTimeout is specified in the Rivet configuration' { + Start-RivetTest -CommandTimeout 60 + @' + function Push-Migration + { + Invoke-Ddl 'select 1' + } + function Pop-Migration + { + Invoke-Ddl 'select 1' + } +'@ | New-TestMigration -Name 'SetCommandTimeout' + + $m = Get-Migration -ConfigFilePath $RTConfigFilePath + $Global:Error.Count | Should -Be 0 + $m | Should -BeOfType ([Rivet.Migration]) + $m.PopOperations[0].CommandTimeout | Should -Be 60 + $m.PushOperations[0].CommandTimeout | Should -Be 60 + } } Describe 'Get-Migration.when excluding migrations' { diff --git a/Test/Get-RivetConfig.Tests.ps1 b/Test/Get-RivetConfig.Tests.ps1 index c3187c83..8f6ce7d4 100644 --- a/Test/Get-RivetConfig.Tests.ps1 +++ b/Test/Get-RivetConfig.Tests.ps1 @@ -479,7 +479,25 @@ Describe 'Get-RivetConfig.when databases have a custom order' { { "SqlServerName": ".\\Rivet", "DatabasesRoot": "Databases", - "DatabaseOrder": [ "CCC", "BBB", "ZZZ" ] + "Databases": [ "CCC", "BBB" ] +} +'@ + $config = WhenGettingConfig + It ('should order databases') { + $config.Databases.Count | Should -Be 2 + $config.Databases[0].Name | Should -Be 'CCC' + $config.Databases[1].Name | Should -Be 'BBB' + } +} + +Describe 'Get-RivetConfig.when databases have a custom order with a wild card' { + Init + GivenDatabase 'AAA','BBB','CCC','DDD' + GivenConfig @' +{ + "SqlServerName": ".\\Rivet", + "DatabasesRoot": "Databases", + "Databases": [ "CCC", "BBB", "*" ] } '@ $config = WhenGettingConfig @@ -492,6 +510,45 @@ Describe 'Get-RivetConfig.when databases have a custom order' { } } +Describe 'Get-RivetConfig.when databases have no custom order' { + Init + GivenDatabase 'AAA','BBB','CCC','DDD' + GivenConfig @' +{ + "SqlServerName": ".\\Rivet", + "DatabasesRoot": "Databases" +} +'@ + $config = WhenGettingConfig + It ('should order databases') { + $config.Databases.Count | Should -Be 4 + $config.Databases[0].Name | Should -Be 'AAA' + $config.Databases[1].Name | Should -Be 'BBB' + $config.Databases[2].Name | Should -Be 'CCC' + $config.Databases[3].Name | Should -Be 'DDD' + } +} + +Describe 'Get-RivetConfig.when databases have a custom order but one doesn''t exist' { + Init + GivenDatabase 'AAA','BBB','CCC','DDD' + GivenConfig @' +{ + "SqlServerName": ".\\Rivet", + "DatabasesRoot": "Databases", + "Databases": [ "CCC", "BBB", "ZZZ" ] +} +'@ + $config = WhenGettingConfig -ErrorAction SilentlyContinue + It ('should order databases') { + $config.Databases.Count | Should -Be 2 + $config.Databases[0].Name | Should -Be 'CCC' + $config.Databases[1].Name | Should -Be 'BBB' + } + $errorMessage = Join-Path -Path $config.DatabasesRoot -ChildPath 'ZZZ' + $Global:Error | Should -Match "database named ""ZZZ"" at ""$($errorMessage.replace('\','\\'))"" does not exist" +} + Describe 'Get-RivetConfig.when plugins root has a wildcard' { It 'should resolve to actual path' { Init diff --git a/Test/RivetTest/Functions/Start-RivetTest.ps1 b/Test/RivetTest/Functions/Start-RivetTest.ps1 index 611d8f85..2010dbd3 100644 --- a/Test/RivetTest/Functions/Start-RivetTest.ps1 +++ b/Test/RivetTest/Functions/Start-RivetTest.ps1 @@ -4,11 +4,16 @@ function Start-RivetTest [CmdletBinding()] param( # Optional Parameter to specify a plugin Path - [String[]]$PluginPath, + [String[]] $PluginPath, - [String[]]$IgnoredDatabase, + [String[]] $IgnoredDatabase, - [String[]]$DatabaseName = $RTDatabaseName + [Alias('DatabaseName')] + [String[]] $PhysicalDatabase = $RTDatabaseName, + + [String[]] $ConfigurationDatabase, + + [int] $CommandTimeout = 30 ) Set-StrictMode -Version Latest @@ -28,7 +33,7 @@ function Start-RivetTest } $script:RTDatabasesRoot = Join-Path -Path $RTTestRoot -ChildPath 'Databases' - foreach( $name in $DatabaseName ) + foreach( $name in $PhysicalDatabase ) { $script:RTDatabaseRoot = Join-Path $RTDatabasesRoot $name $script:RTDatabaseMigrationRoot = Join-Path -Path $RTDatabaseRoot -ChildPath 'Migrations' @@ -65,14 +70,27 @@ function Start-RivetTest $IgnoreClause = ',IgnoreDatabases: [ "{0}" ]' -f ($IgnoredDatabase -join '", "') } - @" + $content = @" { SqlServerName: '$($RTServer.Replace('\', '\\'))', - DatabasesRoot: '$($RTDatabasesRoot.Replace('\','\\'))' + DatabasesRoot: '$($RTDatabasesRoot.Replace('\','\\'))', + CommandTimeout: $($CommandTimeout) +"@ + + if( $ConfigurationDatabase ) + { + $content += @" +, + Databases: [ "$($ConfigurationDatabase -join """, """)" ] +"@ + } + + $content += @" $PluginPathClause $IgnoreClause } -"@ | Set-Content -Path $RTConfigFilePath +"@ + $content | Set-Content -Path $RTConfigFilePath Write-RTTiming ('Start-RivetTest END') } diff --git a/appveyor.yml b/appveyor.yml index 27ec8f43..ff34c359 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -53,13 +53,19 @@ for: only: - job_group: ps build_script: - - ps: .\init.ps1 -SqlServerName "$($env:MSSQL_INSTANCE_NAME)" - - ps: .\build.ps1 + - ps: | + $ProgressPreference = 'SilentlyContinue' + iwr https://raw.githubusercontent.com/webmd-health-services/Prism/main/Scripts/init.ps1 | iex | Format-Table + .\init.ps1 -SqlServerName "$($env:MSSQL_INSTANCE_NAME)" + .\build.ps1 # Build in PowerShell - matrix: only: - job_group: pwsh build_script: - - pwsh: ./init.ps1 -SqlServerName "$($env:MSSQL_INSTANCE_NAME)" - - pwsh: ./build.ps1 \ No newline at end of file + - pwsh: | + $ProgressPreference = 'SilentlyContinue' + iwr https://raw.githubusercontent.com/webmd-health-services/Prism/main/Scripts/init.ps1 | iex | Format-Table + ./init.ps1 -SqlServerName "$($env:MSSQL_INSTANCE_NAME)" + ./build.ps1 diff --git a/init.ps1 b/init.ps1 index 15d29c41..90c56caf 100644 --- a/init.ps1 +++ b/init.ps1 @@ -14,44 +14,3 @@ Set-StrictMode -Version 'Latest' $InformationPreference = 'Continue' $SqlServerName | Set-Content -Path 'Test\Server.txt' - -# Run in a background job so that old PackageManagement assemblies don't get loaded. -$job = Start-Job { - $InformationPreference = 'Continue' - $psGalleryRepo = Get-PSRepository -Name 'PSGallery' - $repoToUse = $psGalleryRepo.Name - # On Windows 2012 R2, Windows PowerShell 5.1, and .NET 4.6.2, PSGallery's URL ends with a '/'. - if( -not $psGalleryRepo -or $psgalleryRepo.SourceLocation -ne 'https://www.powershellgallery.com/api/v2' ) - { - $repoToUse = 'PSGallery2' - Register-PSRepository -Name $repoToUse ` - -InstallationPolicy Trusted ` - -SourceLocation 'https://www.powershellgallery.com/api/v2' ` - -PackageManagementProvider $psGalleryRepo.PackageManagementProvider - } - - Write-Information -MessageData 'Installing latest version of PowerShell module Prism.' - Install-Module -Name 'Prism' -Scope CurrentUser -Repository $repoToUse -AllowClobber -Force - - if( -not (Get-Module -Name 'PackageManagement' -ListAvailable | Where-Object 'Version' -eq '1.4.7') ) - { - Write-Information -MessageData 'Installing PowerShell module PackageManagement 1.4.7.' - Install-Module -Name 'PackageManagement' -RequiredVersion '1.4.7'-Repository $repoToUse -AllowClobber -Force - } - - if( -not (Get-Module -Name 'PowerShellGet' -ListAvailable | Where-Object 'Version' -eq '2.2.5') ) - { - Write-Information -MessageData 'Installing PowerShell module PowerShellGet 2.2.5.' - Install-Module -Name 'PowerShellGet' -RequiredVersion '2.2.5' -Repository $repoToUse -AllowClobber -Force - } -} - -if( (Get-Command -Name 'Receive-Job' -ParameterName 'AutoRemoveJob') ) -{ - $job | Receive-Job -AutoRemoveJob -Wait -} -else -{ - $job | Wait-Job | Receive-Job - $job | Remove-Job -} diff --git a/whiskey.yml b/whiskey.yml index c40ce0b7..eb235447 100644 --- a/whiskey.yml +++ b/whiskey.yml @@ -1,7 +1,7 @@ PublishOn: - prerelease - master - + Build: - PowerShell: IfExists: env:MSSQL_SERVICE_NAME @@ -13,8 +13,9 @@ Build: Path: Rivet\Rivet.psd1 PowerShellModuleName: Rivet Prerelease: - - "*/*": alpha1 - - develop: rc1 + - main: "" + - master: "" + - "*": rc1 # Update the AppVeyor build/version number. - Exec: @@ -60,7 +61,7 @@ Build: - MergeFile: OnlyBy: BuildServer IfExists: Rivet\Functions\*.ps1 - Path: + Path: - Test\RivetTest\Functions\*.ps1 - Test\RivetTest\RivetTest.Exports.ps1 DestinationPath: Test\RivetTest\RivetTest.psm1