functions/Invoke-DBRenameConstraints.ps1

#Requires -Modules Invoke-SqlCmd2

function Invoke-DBRenameConstraints {
    <#
    .SYNOPSIS
        Will rename all indexes and constraints to match naming conventions.
 
    .DESCRIPTION
        Will rename all indexes and constraints to match naming conventions. Any constraint name that already matches the expected
        naming convention will be skipped.
 
    .PARAMETER ServerInstance
        The sql server instance to connect to.
 
    .PARAMETER Databases
        The database.
 
    .PARAMETER Credentials
        Specifies credentials to connect to the database with. If not supplied then a trusted connection will be used.
 
    .PARAMETER IncludeSchemaInNames
        If enabled then all names will include the schema as part of the name.
 
    .PARAMETER Force
        If enabled then all constraint names will be renamed even if they match the expected naming conventions.
 
    .PARAMETER CustomGetObjectName
        This script block can be passed in to override the naming convention used.
 
        The method signature is as follows: function GetObjectName($obj, [switch]$IncludeSchemaInNames)
 
        Note: Each of the details properties holds different values based upon object type
 
        $obj is an objection with the following properties:
            schema_name: The schema name of the object
            table_name: The name of the view or table parent object
            object_name: The name of the constraint or index.
            details1:
                C : The column name used or null if the column could not be determined
                D : The column name used or null if the column could not be determined
                FK : The schema of the remote table name
                Index : The first column used in the index key
                PK : The first column used in the index key
            details2:
                C : NULL
                D : NULL
                FK : The table name of the remote table name
                Index : A full list of the columns used in the index comma delimited
                PK : A full list of the columns used in the index comma delimited
            details3:
                C : NULL
                D : NULL
                FK : NULL
                Index : The detailed type of the index
                PK : The detailed type of the index
            type: The type of object
 
    .EXAMPLE
        PS> .\Invoke-DBRenameConstraints -ServerInstance "servername" -Database "AdventureWorks2012"
 
    .EXAMPLE
        PS> .\Invoke-DBRenameConstraints -ServerInstance "servername" -Database "AdventureWorks2012" -UserName "user.name" -Password "ilovelamp"
 
    .LINK
        https://github.com/tcartwright/tcdbtools
 
    .NOTES
        Author: Tim Cartwright
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$ServerInstance,
        [Parameter(Mandatory=$true)]
        [string[]]$Databases,
        [pscredential]$Credentials,
        [switch]$IncludeSchemaInNames,
        [switch]$Force,
        [scriptblock]$CustomGetObjectName
    )

    begin {
        $sqlCon = InitSqlObjects -ServerInstance $ServerInstance -Database "master" -Credentials $Credentials
        $SqlCmdArguments = $sqlCon.SqlCmdArguments

        $connection = GetSQLConnection -ServerInstance $ServerInstance -Database "master" -Credentials $Credentials
        $connection.Open();
        $command = $connection.CreateCommand()
        $command.CommandType = "Text";

        $query = "
            SELECT [t].[schema_name],
                   [t].[table_name],
                   [t].[object_name],
                   [t].[details1],
                   [t].[details2],
                   [t].[details3],
                   RTRIM([t].[type]) AS [type]
            FROM (
                SELECT
                        [schema_name] = SCHEMA_NAME(fk.[schema_id]),
                        [table_name] = OBJECT_NAME(fk.[parent_object_id]),
                        [object_name] = fk.[name],
                        [details1] = OBJECT_SCHEMA_NAME(fk.[referenced_object_id]),
                        [details2] = OBJECT_NAME(fk.[referenced_object_id]),
                        [details3] = NULL,
                        [o].[type]
                    FROM sys.[foreign_keys] fk
                    INNER JOIN sys.[objects] o ON fk.[object_id] = o.[object_id]
                    WHERE OBJECTPROPERTY(fk.[parent_object_id], 'IsMSShipped') = 0
 
                    UNION ALL
 
                    SELECT
                        [schema_name] = SCHEMA_NAME(o.[schema_id]),
                        [table_name] = OBJECT_NAME(i.[object_id]),
                        [object_name] = i.[name],
                        [details1] = COL_NAME(i.[object_id], ic.[column_id]),
                        [details2] = fn.[names],
                        [details3] = CASE
                                                    WHEN i.[type] = 1 THEN 'Clustered index'
                                                    WHEN i.[type] = 2 THEN 'Nonclustered unique index'
                                                    WHEN i.[type] = 3 THEN 'XML index'
                                                    WHEN i.[type] = 4 THEN 'Spatial index'
                                                    WHEN i.[type] = 5 THEN 'Clustered columnstore index'
                                                    WHEN i.[type] = 6 THEN 'Nonclustered columnstore index'
                                                    WHEN i.[type] = 7 THEN 'Nonclustered hash index'
                                                END,
                        [type] = CASE
                                                    WHEN i.[is_unique_constraint] = 1 THEN 'UQ'
                                                    WHEN i.[is_primary_key] = 1 THEN 'PK'
                                                    WHEN i.[is_unique_constraint] = 1 THEN 'UX'
                                                    ELSE 'NC'
                                                END
                    FROM sys.[indexes] i
                    INNER JOIN sys.[objects] o ON i.[object_id] = o.[object_id]
                    INNER JOIN sys.[index_columns] AS [ic]
                        ON [ic].[object_id] = [i].[object_id]
                            AND [ic].[index_id] = [i].[index_id]
                            AND ic.[index_column_id] = 1
                    CROSS APPLY (
                        SELECT STUFF((
                            SELECT CONCAT(', ', COL_NAME(i.[object_id], ic2.[column_id]))
                            FROM sys.[index_columns] AS [ic2]
                                WHERE [ic2].[object_id] = [i].[object_id]
                                AND [ic2].[index_id] = [i].[index_id]
                            FOR XML PATH('')
                        ), 1, 2, '') AS [names]
                    ) fn
                    WHERE i.type > 0
                        AND OBJECTPROPERTY(o.[object_id], 'IsMSShipped') = 0
                        AND OBJECTPROPERTYEX(o.[object_id], 'BaseType') <> 'TT' -- ignore table types as their constraints cannot be named
 
 
                    UNION ALL
 
                    SELECT
                        [schema_name] = SCHEMA_NAME(o.[schema_id]),
                        [table_name] = OBJECT_NAME(s.[id]),
                        [object_name] = o.[name],
                        [details1] = COL_NAME(s.[id], s.[colid]),
                        [details2] = NULL,
                        [details3] = NULL,
                        [o].[type]
                    FROM sys.[sysconstraints] s
                    INNER JOIN sys.[objects] o ON s.[constid] = o.[object_id]
                    WHERE o.type NOT IN ('F', 'PK', 'UQ')
                        AND OBJECTPROPERTY(s.[id], 'IsMSShipped') = 0
                        AND OBJECTPROPERTYEX(s.[id], 'BaseType') <> 'TT' -- ignore table types as their constraints cannot be named
            ) t
            ORDER BY [t].[schema_name],
                [t].[table_name],
                [t].[object_name]"


        $sql = "EXEC sys.sp_rename @objname=N'{0}', @newname=N'{1}', @objtype=N'{2}';`r`n"
        $tempRenames = @{}
        $renames = @{}
        $output = [System.Collections.ArrayList]::new()
    }

    process {
        foreach ($Database in $Databases) {
            $SqlCmdArguments.Database = $Database
            $results = Invoke-Sqlcmd @SqlCmdArguments -Query $query -OutputAs DataRows
            $grouped = $results | Group-Object -Property schema_name, table_name

            foreach ($item in $grouped) {
                $renames.Clear()
                $tempRenames.Clear()
                $objectName = $item.Name -replace ", ", "."

                foreach ($grp in $item.Group) {
                    if (-not $CustomGetObjectName) {
                        $newName = GetObjectName -obj $grp -IncludeSchemaInNames:$IncludeSchemaInNames.IsPresent
                    } else {
                        $newName = $CustomGetObjectName.Invoke($grp, $IncludeSchemaInNames.IsPresent)
                    }

                    if ($renames.ContainsKey($newName)) {
                        for ($i = 1; $i -lt 1000; $i++) {
                            $suffix = "000$i"
                            $suffix = $suffix.Substring($suffix.Length - 3)
                            $tmpName = "$($newName)_$suffix"
                            if (-not $renames.ContainsKey($tmpName)) {
                                $newName = $tmpName
                                break;
                            }
                        }
                    }

                    # unless force is present, do not rename this, as it already matches our desired name
                    if (-not $Force.IsPresent -and $newName -ieq $grp.object_name) {
                        # store this, so the numbers will work properly in the for loop above
                        $renames.Add($newName, "") | Out-Null
                        continue
                    }

                    $tempName = "tmp_$([Guid]::NewGuid().ToString("N"))"
                    # handle the crappy case where their old name had brackets in it. :|
                    $oldName = $grp.object_name -replace "\[", "\[\[" -replace "\]", "\]\]"

                    # we must first rename the constraints to some super generic name to avoid name collisions, then immediately rename it back
                    if ($grp.type.Trim() -ine "NC") {
                        $tempSql = $sql -f "[$($grp.schema_name)].[$oldName]", "$tempName", "OBJECT"
                        $tempRenames.Add($newName, $tempSql) | Out-Null

                        $tempSql = $sql -f "[$($grp.schema_name)].[$($tempName)]", $newName, "OBJECT"
                        $renames.Add($newName, $tempSql) | Out-Null
                    } else {
                        $tempSql = $sql -f "[$($grp.schema_name)].[$($grp.table_name)].[$oldName]", "$tempName", "INDEX"
                        $tempRenames.Add($newName, $tempSql) | Out-Null

                        $tempSql = $sql -f "[$($grp.schema_name)].[$($grp.table_name)].[$tempName]", "$newName", "INDEX"
                        $renames.Add($newName, $tempSql) | Out-Null
                    }

                    $output.Add([PSCustomObject] @{
                        Database = $Database
                        ObjectName = $objectName
                        Type = $grp.Type
                        OldConstraintName = $oldName
                        NewConstraintName = $newName
                    }) | Out-Null
                }
                if ($renames.Count -gt 0 -and "$($renames.Values)".Trim().Length -gt 0) {
                    $renameSql = "
/***********************************************************************************************************/
/*********************** Start renames for $Database $objectName ************************************/
/***********************************************************************************************************/
USE [$($Database)]
SET XACT_ABORT ON
/***********************************************************************************************************/
/***Rename the constraints for $Database $objectName to temporary names to avoid collisions**********/
/***********************************************************************************************************/
$($tempRenames.Values)
/***********************************************************************************************************/
/***Rename the constraints for $Database $objectName to their new permanent names********************/
/***********************************************************************************************************/
$($renames.Values)"


                    $ErrorActionPreference = "Stop"
                    try {
                        Write-Information $renameSql
                        $command.CommandText = $renameSql;
                        $command.ExecuteNonQuery() | Out-Null
                    } catch {
                        Write-InformationColored $_.Exception.Message -ForegroundColor Red
                    }
                    $ErrorActionPreference = "Continue"
                } else {
                    Write-InformationColored "No renames available for [$Database] object: $($objectName)" -ForegroundColor Yellow
                }
            }
        }
    }

    end {
        if ($command) { $command.Dispose() }
        if ($connection) { $connection.Dispose() }
        return $output | Sort-Object Database, ObjectName, NewConstraintName
    }
}