ConvertFrom-DbSmo.ps1
<#
.SYNOPSIS .DESCRIPTION .PARAMETER .INPUTS .OUTPUTS .EXAMPLE #> function ConvertFrom-DbSmo { [Cmdletbinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $InputObject, [System.Data.DataSet] $OutputObject, [int] $Depth = 0, # If there's no Urn property on the object we received, these "prior" properties are used to construct a path for # a) checking against exclusions and indirectly # b) the table name [string] $SubstitutePath, $ParentPrimaryKeyColumns, $ServerName, $MaxDepth = 10 ) if ($null -eq $OutputObject) { $OutputObject = New-Object System.Data.DataSet $OutputObject.EnforceConstraints = $false } $Depth++ $tab = "`t" * $Depth # Do a depth check. If this triggered it would mean we did something really wrong because everything should be # accessible within the depth I've selected. if ($Depth -gt $maxDepth) { throw "Max depth exceeded, this shouldn't have happened..." } # Work out a "path". This is something like /Server/Database/User. We may get to some type which doesn't have # its own Urn so in those cases we can fall back to the parent path plus property name. if (!$InputObject.psobject.Properties["Urn"]) { $path = $SubstitutePath Write-Verbose "$($tab)Working on substitute path of $path" } else { $urn = $InputObject.Urn $path = $urn.XPathExpression.ExpressionSkeleton Write-Verbose "$($tab)Working on $urn, the skeleton path is $path" } # These are table renames for conflicts and readability. I don't think it will work if you renamed # one that has a foreign key dependency on it though. If you really wanted to do this you'd need # to work out how to make sub tables pick up this name; it gets extracted from the Urn which is why # it wouldn't work. Unless we switched that to use the path instead, and overwrote the path; here # and on the sub tables. I don't do that because splitting on the path breaks easily because it's # based on / which can show in lots of properties. The XPath doesn't have this issue. But we could # convert the $path variable to an array instead and then join it for comparisons. # # On second thoughts, the past primary key is fine. The new foreign key is fine. The foreign key # name is fine (it gets the name from the foreign key table). All that would be wrong is the name # of the new key for the foreign key because it's based on the XPath not on the past table name. # That could be fixed easily... # # These only rename TABLES, not PROPERTIES. $performancePath = Get-Date switch ($path) { "Server/Configuration" { $tableName = "ServerConfiguration" break } # Rename for readability "Server/Mail/ConfigurationValue" { $tableName = "MailConfigurationValue" break } # Rename for readability "Server/UserOption" { $tableName = "ServerUserOption" break } # Schedule = Server/JobServer/Job/SharedSchedule "Server/JobServer/Job/Schedule" { $tableName = "JobSchedule" break } # Login = Server/Login "Server/LinkedServer/Login" { $tableName = "LinkedServerLogin" # Server/Login goes under just Login break } "Server/Database/Certificate" { $tableName = "DatabaseCertificate" break } "Server/Database/SymmetricKey" { $tableName = "DatabaseSymmetricKey" break } "Server/Database/DefaultFullTextLanguage" { $tableName = "DatabaseDefaultFullTextLanguage" break } # These are excluded so shouldn't ever occur anyway # Don't use DefaultLanguage "Server/Database/DefaultLanguage" { $tableName = "DatabaseDefaultLanguage" break } "Server/Database/User/DefaultLanguage" { $tableName = "UserDefaultLanguage" break } # Don't use ServiceBroker "Server/Database/ServiceBroker" { $tableName = "DatabaseServiceBroker" break } "Server/Endpoint/ServiceBroker" { $tableName = "EndpointServiceBroker" break } # Don't use Role "Server/Role" { $tableName = "ServerRole" break } "Server/Database/Role" { $tableName = "DatabaseRole" break } # Cpus = Server/AffinityInfo/Cpus "Server/AffinityInfo/NumaNodes/Cpus" { $tableName = "NumaNodesCpus" break } "Server/ResourceGovernor/ResourcePool/ResourcePoolAffinityInfo/Schedulers" { $tableName = "ResourcePoolSchedulers" # Not a typo, a standardization break } "Server/ResourceGovernor/ResourcePool/ResourcePoolAffinityInfo/Schedulers/Cpu" { $tableName = "ResourcePoolSchedulersCpus" # Not a typo, a standardization break } "Server/ResourceGovernor/ResourcePool/ResourcePoolAffinityInfo/NumaNodes/Cpus" { $tableName = "ResourcePoolNumaNodesCpus" break } # 2016 Additions "Server/ResourceGovernor/ExternalResourcePool/ExternalResourcePoolAffinityInfo/Cpus" { # Not a typo, they standardised it $tableName = "ExternalResourcePoolCpus" break } "Server/ResourceGovernor/ExternalResourcePool/ExternalResourcePoolAffinityInfo/NumaNodes/Cpus" { $tableName = "ExternalResourcePoolNumaNodesCpus" break } # NumaNodes = Server/AffinityInfo/NumaNodes "Server/ResourceGovernor/ResourcePool/ResourcePoolAffinityInfo/NumaNodes" { $tableName = "ResourcePoolNumaNodes" break } "Server/ResourceGovernor/ExternalResourcePool/ExternalResourcePoolAffinityInfo/NumaNodes" { $tableName = "ExternalResourcePoolNumaNodes" break } # Don't use IPAddress "ManagedComputer/ServerInstance/ServerProtocol/IPAddress" { $tableName = "ServerProtocolIPAddress" break } "ManagedComputer/ServerInstance/ServerProtocol/IPAddress/IPAddress" { $tableName = "ServerProtocolIPAddressDetail" break } # Readability "Server/JobServer/Job/Step" { $tableName = "JobStep" break } "Server/Endpoint/Payload" { $tableName = "EndpointPayload" break } "Server/Endpoint/Soap" { $tableName = "EndpointSoap" break } "Server/Endpoint/DatabaseMirroring" { $tableName = "EndpointDatabaseMirroring" break } "Server/Endpoint/Protocol" { $tableName = "EndpointProtocol" break } "Server/Endpoint/Http" { $tableName = "EndpointHttp" break } "Server/Endpoint/Tcp" { $tableName = "EndpointTcp" break } "Server/Endpoint/Tcp/ListenerIPAddress" { $tableName = "EndpointListenerIPAddress" break } "Server/JobServer/Schedule" { $tableName = "JobServerSchedule" break } "Server/JobServer/ProxyAccount" { $tableName = "JobServerProxyAccount" break } "Server/JobServer/AlertSystem" { $tableName = "JobServerAlertSystem" break } "Server/JobServer/JobCategory" { $tableName = "JobServerJobCategory" break } "Server/JobServer/Alert" { $tableName = "JobServerAlert" break } "Server/JobServer/Operator" { $tableName = "JobServerOperator" break } "Server/JobServer/AlertCategory" { $tableName = "JobServerAlertCategory" break } "Server/JobServer/OperatorCategory" { $tableName = "JobServerOperatorCategory" break } "Server/ResourceGovernor/ResourcePool/WorkloadGroup" { $tableName = "ResourcePoolWorkloadGroup" break } <# # This is broken "ManagedComputer/Service/Dependencies" { $tableName = "ServiceDependencies" break } #> "Server/Database/FileGroup/File" { $tableName = "DatabaseFile" break } "Server/Database/LogFile" { $tableName = "DatabaseLogFile" break } "Server/Database/FileGroup" { $tableName = "DatabaseFileGroup" break } # Enum methods "Server/Database/User/EnumRoles" { $tableName = "UserRole" break } "Server/Database/User/EnumObjectPermissions" { $tableName = "UserPermission" break } "Server/Database/User/EnumObjectPermissions/PermissionType" { # Child of above $tableName = "UserPermissionType" break } "Server/Role/EnumMemberNames" { # EnumServerRoleMembers is deprecated $tableName = "ServerRoleMember" break } "Server/Role/EnumObjectPermissions" { $tableName = "ServerRolePermission" break } "Server/Login/EnumObjectPermissions" { $tableName = "LoginPermission" break } default { # Configuration entries all follow the same pattern. We flatten them into one table. if ($path -like "Server/Configuration/*") { $tableName = "ServerConfiguration" } else { $tableName = $path -split "/" | Select-Object -Last 1 } } } "(Path Switch)" | Add-PerformanceRecord $performancePath # We can pull out the existing table or create a new one if ($OutputObject.Tables[$tableName]) { Write-Verbose "$($tab)Retrieving table $tableName" $table = $OutputObject.Tables[$tableName] } else { Write-Verbose "$($tab)Adding table $tableName" $table = $OutputObject.Tables.Add($tableName) } # We need to populate primary keys (and add the columns if necessary) Write-Verbose "$($tab)Preparing primary keys" $performancePrimaryKey = Get-Date # Create a row but this isn't added to the table until all properties (and sub properties) have been processed on the row. # But the row must be created BEFORE we calculate primary keys, so we can add the values for each key item. $row = $table.NewRow() $primaryKeyColumns = New-Object System.Collections.ArrayList $foreignKeyColumns = New-Object System.Collections.ArrayList # Primary key constraints are only made on the Urn, even if it's not the most current one. We apply fixups later. for ($i = 0; $i -lt $urn.XPathExpression.Length; $i++) { $key = $urn.XPathExpression.Item($i) # Iterate through each part of the URN; e.g. the Server part, the Database part, the User part. foreach ($keyProperty in $key.FixedProperties.GetEnumerator()) { if ($i -eq ($urn.XPathExpression.Length - 1) -and $InputObject.psobject.Properties["Urn"]) { # If we are on the last part of the Urn, and the current row has a Urn, we use the proper name # (because this last name is the one that will be used on the current row as a property already) $keyPropertyName = $keyProperty.Name } else { # Otherwise we prefix names with the parent path name. We do this so that we don't get collisions # on things like Name; instead renaming them to ServerName, DatabaseName, etc, in the current row. # Also, if we were on the last step, but there is no Urn, then it means we still need to do this; # as the current row will be using a different current property name already, it's just not part # of the key yet (as far as we know, it will be "fixed" by adding it manually a bit later). $parentColumn = $ParentPrimaryKeyColumns[$primaryKeyColumns.Count] if (($ParentPrimaryKeyColumns[0].Table.Constraints | Where-Object { $_ -is [System.Data.ForeignKeyConstraint] } | Select-Object -ExpandProperty Columns) -contains $parentColumn) { $keyPropertyName = $parentColumn.ColumnName } else { $keyPropertyName = "$($ParentPrimaryKeyColumns[0].Table.TableName)$($parentColumn.ColumnName)" } } # Examples: # /Server Key = Name # /Server/Database Key = ServerName, Name # /Server/Mail/MailProfile = ServerName, Name (as Mail does not have a key) # /Server/Database/User/DefaultLanguage (no Urn) = ServerName, DatabaseName, UserName # This is the key itself if ($tableName -eq "Server" -and $keyPropertyName -eq "Name") { $keyPropertyValue = $InputObject.Name $ServerName = $InputObject.Name $maxLength = 272 } else { if ($keyPropertyName -eq "ServerName") { $keyPropertyValue = $ServerName $maxLength = 272 } elseif (($tableName -eq "ManagedComputer" -and $keyPropertyName -eq "Name") -or $keyPropertyName -eq "ManagedComputerName") { $keyPropertyValue = $keyProperty.Value.Value $maxLength = 255 } else { $keyPropertyValue = $keyProperty.Value.Value $maxLength = 128 } # The Xml parser does not propery decode the additional quotations; for example on a Step.JobName if ($keyPropertyValue -is [string]) { $keyPropertyValue = $keyPropertyValue.Replace("''", "'") } } if (!$table.Columns[$keyPropertyName]) { $column = New-Object System.Data.DataColumn $column.ColumnName = $keyPropertyName # It recognises all of these automatically Number but I populate them for prosperity anyway $column.DataType = switch ($keyProperty.Value.ObjType) { "String" { "System.String" } "Boolean" { "System.Boolean" } "Number" { "System.Int32" } } # Not a bug, use -eq instead of -is if ($column.DataType -eq [string]) { $column.MaxLength = $maxLength } [void] $table.Columns.Add($column) Write-Verbose "$($tab)Key $keyPropertyName added" } else { Write-Verbose "$($tab)Key $keyPropertyName already exists" } [void] $primaryKeyColumns.Add($table.Columns[$keyPropertyName]) # Our local foreign key columns are everything except the last key (unless we have no Urn, in which case the last key doesn't exist yet) if ($i -ne ($urn.XPathExpression.Length - 1) -or !$InputObject.psobject.Properties["Urn"]) { [void] $foreignKeyColumns.Add($table.Columns[$keyPropertyName]) } if ($null -eq $keyPropertyValue) { throw "Null value in primary key, this shouldn't happen" } else { $row[$keyPropertyName] = $keyPropertyValue } } } # Finished looping primary keys "(Primary Key)" | Add-PerformanceRecord $performancePrimaryKey $performanceProperties = Get-Date # Get a list of properties to process; but remove the ones that match the wildcards in our exclusion list $performanceExclude = Get-Date $properties = New-Object System.Collections.ArrayList $InputObject.psobject.Properties | Where-Object { $DbSmoPropertyExclusions -notcontains $_.Name -and $DbSmoPathExclusions -notcontains "$path/$($_.Name)" } | ForEach-Object { [void] $properties.Add($_) } # Smo # Some of the complexity of the checks here are to make sure we don't do stuff on a "Creating" object, which don't react well to reading and writing properties. if ((!$InputObject.psobject.Properties["State"] -or $InputObject.State -ne "Creating") -and $InputObject.psobject.Properties["Properties"] -and $InputObject.psobject.Properties["Properties"].TypeNameOfValue -eq "Microsoft.SqlServer.Management.Smo.SqlPropertyCollection") { $newProperties = for ($i = 0; $i -lt $InputObject.Properties.Count; $i++) { <# We can't use GetEnumerator() because it sometimes throws a failure in fn_getProcessorUsage_internal where weird .NET assemblies exist on the system. So we iterate them by number, and skip the bad ones. #> try { $InputObject.Properties[$i] | Where-Object { $_ -and ($properties | Select-Object -ExpandProperty Name) -notcontains $_.Name -and $DbSmoPropertyExclusions -notcontains $_.Name -and $DbSmoPathExclusions -notcontains "$path/$($_.Name)" } } catch { Write-Verbose "Skipped a property because it gave an error, $_" } } $newProperties | ForEach-Object { Write-Verbose "Specially added $($_.Name)"; [void] $properties.Add($_) } } if ((!$InputObject.psobject.Properties["State"] -or $InputObject.State -ne "Creating") -and $InputObject.psobject.Properties["AdvancedProperties"] -and $InputObject.psobject.Properties["AdvancedProperties"].TypeNameOfValue -eq "Microsoft.SqlServer.Management.Smo.SqlPropertyCollection") { $newProperties += $InputObject.AdvancedProperties.GetEnumerator() | Where-Object { $_ -and ($properties | Select-Object -ExpandProperty Name) -notcontains $_.Name -and $DbSmoPropertyExclusions -notcontains $_.Name -and $DbSmoPathExclusions -notcontains "$path/$($_.Name)" } $newProperties | ForEach-Object { Write-Verbose "Very specially added $($_.Name)"; [void] $properties.Add($_) } } # Wmi, these have a different Type name if ((!$InputObject.psobject.Properties["State"] -or $InputObject.State -ne "Creating") -and $InputObject.psobject.Properties["Properties"] -and $InputObject.psobject.Properties["Properties"].TypeNameOfValue -eq "Microsoft.SqlServer.Management.Smo.PropertyCollection") { # I added the check for psobject properties name because the Server/Configuration is a special case below and does not have a Name property; doing it here ruins stuff $newProperties = $InputObject.Properties.GetEnumerator() | Where-Object { $_ -and ($properties | Select-Object -ExpandProperty Name) -notcontains $_.Name -and $DbSmoPropertyExclusions -notcontains $_.Name -and $DbSmoPathExclusions -notcontains "$path/$($_.Name)" } $newProperties | ForEach-Object { Write-Verbose "Specially added $($_.Name)"; [void] $properties.Add($_) } } if ((!$InputObject.psobject.Properties["State"] -or $InputObject.State -ne "Creating") -and $InputObject.psobject.Properties["AdvancedProperties"] -and $InputObject.psobject.Properties["AdvancedProperties"].TypeNameOfValue -eq "Microsoft.SqlServer.Management.Smo.PropertyCollection") { $newProperties = $InputObject.AdvancedProperties.GetEnumerator() | Where-Object { $_ -and ($properties | Select-Object -ExpandProperty Name) -notcontains $_.Name -and $DbSmoPropertyExclusions -notcontains $_.Name -and $DbSmoPathExclusions -notcontains "$path/$($_.Name)" } $newProperties | ForEach-Object { Write-Verbose "Very specially added $($_.Name)"; [void] $properties.Add($_) } } "(Performance Exclude)" | Add-PerformanceRecord $performanceExclude # Write-Verbose "$($tab)Properties $($properties | Select-Object -ExpandProperty Name)" $writeRow = $true $recurseProperties = @() <# # Make sure never to remove the Enum* part or we'd be calling random methods! $InputObject.psobject.Methods | Where-Object { $_.Name -Like "Enum*" } | ForEach-Object { if ($path -eq "Server/Database/User" -and @("EnumRoles", "EnumObjectPermissions") -contains $_.Name) { $recurseProperties += $_ } if ($path -eq "Server/Login" -and @("EnumObjectPermissions") -contains $_.Name) { $recurseProperties += $_ } if ($path -eq "Server/Role" -and @("EnumMemberNames", "EnumObjectPermissions") -contains $_.Name) { $recurseProperties += $_ } } #> foreach ($property in $properties.GetEnumerator()) { try { $propertyName = $property.Name if ($property.psobject.Properties["TypeNameOfValue"]) { $propertyType = $property.TypeNameOfValue } else { $propertyType = $property.Type.ToString() # .GetType().FullName gets a RuntimeType, obviously it's something else } # These are handled as properties on the main object, the real property collection doesn't need to be touched if ($propertyType.StartsWith("Microsoft.SqlServer.Management.Smo.") -and $propertyType.EndsWith("PropertyCollection")) { Write-Verbose "$($tab)Completely skipping $propertyName as it is a property collection" continue } # SMO has a bug which throws an exception if you try to iterate through this property. Instead we redirect it to use # the one in Server/Settings which is more reliable. We already did the exclusion check so it's not impacted here. if ($propertyName -eq "OleDbProviderSettings" -and $propertyType -eq "Microsoft.SqlServer.Management.Smo.OleDbProviderSettingsCollection") { $property = $InputObject.Settings.psobject.Properties["OleDbProviderSettings"] } $propertyValue = $property.Value # This addresses specific Server/Configuration entries which have not been filled out, causing an exception # when you add them to the table while constraints exist. if ($propertyType -eq "Microsoft.SqlServer.Management.Smo.ConfigProperty") { # It's important to use this instead of a check; because UserInstanceTimeout can be a Null value type if ($null -eq $propertyValue -or $propertyValue.Number -eq 0) { Write-Verbose "$($tab)Skipping config property $propertyName with value $propertyValue because it's invalid" continue } else { Write-Verbose "$($tab)Processing config property $propertyName" $OutputObject = ConvertFrom-DbSmo $propertyValue $OutputObject $Depth "$path/$propertyName" $parentPrimaryKeyColumns $ServerName $writeRow = $false continue # We don't return because we want to continue processing all of the other properties in this way. # However we also don't want to write the row at the end because it's empty, so we set a special flag # not to. } } elseif ($propertyValue -is [System.Collections.ICollection] -and $propertyValue -isnot [System.Byte[]]) { Write-Verbose "$($tab)Processing property $propertyName collection" # It's possible for it to be null, which is okay, and worth trying to iterate... maybe... I should test this if ($propertyValue.Count -eq 0) { continue } $recurseProperties += $property continue } else { # We can handle [System.Byte[]] as Varbinary, and we manually skip the collection portion/other properties later if (!$table.Columns[$propertyName]) { $column = New-Object System.Data.DataColumn $column.ColumnName = $propertyName # When adding a column don't jump directly to checking $propertyValue as it may still be null. if ($property.psobject.Properties["MemberType"] -and $property.MemberType -eq "ScriptProperty") { # Used on IpAddressToString; MemberType doesn't exist on Properties/AdvancedProperties $columnDataType = "System.String" } else { $columnDataType = if ($DataTypeSimple -contains $propertyType) { $propertyType } elseif ($DataTypeString -contains $propertyType -or ([type] $propertyType).IsEnum) { "System.String" } } if (!$columnDataType) { # If we don't haev the right data type, then we can't, by definition, add the column Write-Verbose "$($tab)Skipped writing out the raw column for $propertyName because it doesn't look right; it may be recursed instead" if ($null -eq $propertyValue) { continue } else { $recurseProperties += $property continue } } $column.DataType = $columnDataType [void] $table.Columns.Add($column) } # If it's null we don't need to set it because it defaults to [DBNull]::Value anyway (probably). Also, always # maybe sure to check -(n)e(q) $null because $propertyValue could be a boolean, and false's would then not be # written out. if ($null -ne $propertyValue) { Write-Verbose "$($tab)Processing property $propertyName with value $propertyValue" # This is how SMO represents null dates; a 0000 date or a 1900 date. Both are converted to null. if ($propertyValue -isnot [System.DateTime] -or @(599266080000000000, 0) -notcontains $propertyValue.Ticks) { $row[$propertyName] = $propertyValue } } } } catch { if (Test-Error Microsoft.SqlServer.Management.Sdk.Sfc.InvalidVersionEnumeratorException) { # e.g. Availability Groups on lower versions of SQL Server Write-Verbose "$($tab)Property collection not valid on this version." } elseif (Test-Error System.UnauthorizedAccessException) { throw "Administrator (or other) permission required to use WMI." } elseif (Test-Error @{ ErrorCode = "InvalidNamespace" }) { throw "SMO is unable to find WMI endpoint; this could be the SMO 2016 -> 2014/2012 bug, SMO 2014 -> 2012 bug, or SQL Server < 2005 (not supported by SMO)." } elseif (Test-Error @{ Number = 927; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's currently restoring." } elseif (Test-Error @{ Number = 942; Class = 14; State = 4 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's offline." } elseif (Test-Error @{ Number = 945; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail probably because it's part of a mirror/AG and restoring." } elseif (Test-Error @{ Number = 954; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a mirror/AG." } elseif (Test-Error @{ Number = 978; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a AG and has read-intent only." } elseif (Test-Error @{ Number = 3906; Class = 16; State = 1 }) { Write-Verbose "$($tab)Unable to examine this item in detail because the database is read only (often Full-Text catalogs on a secondary in a mirror/AG)." } elseif (Test-Error @{ TargetSite = "System.String GetDbCollation(System.String)" }) { Write-Verbose "$($tab)Likely a database has been set offline but believes it is configured for Auto_Close when it's not. Set the database online, re-disable Auto_Close (even if it's not set), set it back offline. Sometimes the error remains though." } elseif (Test-Error @{ Number = 208; Class = 16; State = 1; Message = "Invalid object name 'sys.resource_governor_external_resource_pools'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Number = 207; Class = 16; State = 1; Message = "Invalid column name 'is_distributed'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } else { throw } } } # Finished first round of adding properties and values "(Properties)" | Add-PerformanceRecord $performanceProperties $path | Add-PerformanceRecord $performanceProperties $performanceConstraints = Get-Date # Do primary key fixups (additional key columns) for properties without a full Urn. this has to be done # after all of the properties have been looped above, otherwise the column won't exist yet (we could # create it but then we need to think of data types again, and duplicates effort). switch ($tableName) { "ServerConfiguration" { # Because we flattened it; it doesn't have a natural key [void] $primaryKeyColumns.Add($table.Columns["Number"]) break } "Cpus" { # Because it doesn't have a Urn; Id is the Id of each single CPU [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "ExternalResourcePoolCpus" { # Because it doesn't have a Urn; Id is the Id of each single CPU [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "NumaNodes" { # Because it doesn't have a Urn; Id is the Id of each single Numa Node [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "NumaNodesCpus" { [void] $primaryKeyColumns.Add($table.Columns["Id"]) [void] $foreignKeyColumns.Add($table.Columns["NumaNodeId"]) break } "ExternalResourcePoolNumaNodes" { # Because it doesn't have a Urn; Id is the Id of each single Numa Node [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "ExternalResourcePoolNumaNodesCpus" { [void] $primaryKeyColumns.Add($table.Columns["Id"]) [void] $foreignKeyColumns.Add($table.Columns["NumaNodeId"]) break } "ResourcePoolSchedulers" { [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "ResourcePoolSchedulersCpus" { # Because it doesn't have a Urn. I think that Id is the Cpu Id in both columns but it wasn't clear. [void] $primaryKeyColumns.Add($table.Columns["Id"]) [void] $foreignKeyColumns.Add($table.Columns["Id"]) break } "ResourcePoolNumaNodes" { [void] $primaryKeyColumns.Add($table.Columns["Id"]) break } "ResourcePoolNumaNodesCpus" { [void] $primaryKeyColumns.Add($table.Columns["Id"]) [void] $foreignKeyColumns.Add($table.Columns["NumaNodeId"]) break } } # If there's no primary key on the table already then we'll add it try { if (!$table.PrimaryKey) { Write-Verbose "$($tab)Creating primary key" [void] $table.Constraints.Add("PK_$tableName", $primaryKeyColumns, $true) # Check we have foreign keys to create (we wouldn't, for example, on Server) and that no foreign key exists yet. if ($foreignKeyColumns -and !($table.Constraints | Where-Object { $_ -is [System.Data.ForeignKeyConstraint] })) { $foreignKeyName = "FK_$($tableName)_$($ParentPrimaryKeyColumns[0].Table.TableName)" Write-Verbose "$($tab)Creating foreign key $foreignKeyName" $foreignKeyConstraint = New-Object System.Data.ForeignKeyConstraint($foreignKeyName, $ParentPrimaryKeyColumns, $foreignKeyColumns) [void] $table.Constraints.Add($foreignKeyConstraint) } } } catch { # Choke point for exceptions throw } "(Constraints)" | Add-PerformanceRecord $performanceConstraints # Part 2 is where we go through and start recursing things try { foreach ($property in $recurseProperties) { $propertyType = $property.MemberType $propertyName = $property.Name $propertyValue = $property.Value if ($propertyType -eq "Method") { <# # For additional safety, make sure it enumerates something if ($propertyName -like "Enum*") { Write-Verbose "$($tab)Processing $propertyName as a method" foreach ($item in $propertyValue.Invoke()) { $OutputObject = ConvertFrom-DbSmo $item $OutputObject $Depth "$path/$propertyName" $primaryKeyColumns $ServerName } } #> } elseif ($propertyValue -is [System.Collections.ICollection]) { Write-Verbose "$($tab)Recursing through $propertyName as a collection" try { foreach ($item in $propertyValue.GetEnumerator()) { try { $OutputObject = ConvertFrom-DbSmo $item $OutputObject $Depth "$path/$propertyName" $primaryKeyColumns $ServerName } catch { # Sweet spot for all error trapping if (Test-Error Microsoft.SqlServer.Management.Sdk.Sfc.InvalidVersionEnumeratorException) { # e.g. Availability Groups on lower versions of SQL Server Write-Verbose "$($tab)Property collection not valid on this version." } elseif (Test-Error System.UnauthorizedAccessException) { throw "Administrator (or other) permission required to use WMI." } elseif (Test-Error @{ ErrorCode = "InvalidNamespace" }) { throw "SMO is unable to find WMI endpoint; this could be the SMO 2016 -> 2014/2012 bug, SMO 2014 -> 2012 bug, or SQL Server < 2005 (not supported by SMO)." } elseif (Test-Error @{ Number = 924; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's in SINGLE_USER mode." } elseif (Test-Error @{ Number = 926; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's in SUSPECT mode." } elseif (Test-Error @{ Number = 927; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's currently restoring." } elseif (Test-Error @{ Number = 942; Class = 14; State = 4 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's offline." } elseif (Test-Error @{ Number = 945; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail probably because it's part of a mirror/AG and restoring." } elseif (Test-Error @{ Number = 954; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a mirror/AG." } elseif (Test-Error @{ Number = 978; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a AG and has read-intent only." } elseif (Test-Error @{ Number = 3906; Class = 16; State = 1 }) { Write-Verbose "$($tab)Unable to examine this item in detail because the database is read only (often Full-Text catalogs on a secondary in a mirror/AG)." } elseif (Test-Error @{ TargetSite = "System.String GetDbCollation(System.String)" }) { Write-Verbose "$($tab)Likely a database has been set offline but believes it is configured for Auto_Close when it's not. Set the database online, re-disable Auto_Close (even if it's not set), set it back offline. Sometimes the error remains though." } elseif (Test-Error @{ Number = 208; Class = 16; State = 1; Message = "Invalid object name 'sys.resource_governor_external_resource_pools'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Number = 207; Class = 16; State = 1; Message = "Invalid column name 'is_distributed'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Message = "Invalid object name 'sys.federations'." }) { Write-Verbose "$($tab)Skipping undefined federations." } else { throw } } } } catch { # Sweet spot for all error trapping if (Test-Error Microsoft.SqlServer.Management.Sdk.Sfc.InvalidVersionEnumeratorException) { # e.g. Availability Groups on lower versions of SQL Server Write-Verbose "$($tab)Property collection not valid on this version." } elseif (Test-Error System.UnauthorizedAccessException) { throw "Administrator (or other) permission required to use WMI." } elseif (Test-Error @{ ErrorCode = "InvalidNamespace" }) { throw "SMO is unable to find WMI endpoint; this could be the SMO 2016 -> 2014/2012 bug, SMO 2014 -> 2012 bug, or SQL Server < 2005 (not supported by SMO)." } elseif (Test-Error @{ Number = 926; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's in SUSPECT mode." } elseif (Test-Error @{ Number = 927; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's currently restoring." } elseif (Test-Error @{ Number = 942; Class = 14; State = 4 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's offline." } elseif (Test-Error @{ Number = 945; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail probably because it's part of a mirror/AG and restoring." } elseif (Test-Error @{ Number = 954; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a mirror/AG." } elseif (Test-Error @{ Number = 978; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a AG and has read-intent only." } elseif (Test-Error @{ Number = 3906; Class = 16; State = 1 }) { Write-Verbose "$($tab)Unable to examine this item in detail because the database is read only (often Full-Text catalogs on a secondary in a mirror/AG)." } elseif (Test-Error @{ TargetSite = "System.String GetDbCollation(System.String)" }) { Write-Verbose "$($tab)Likely a database has been set offline but believes it is configured for Auto_Close when it's not. Set the database online, re-disable Auto_Close (even if it's not set), set it back offline. Sometimes the error remains though." } elseif (Test-Error @{ Number = 208; Class = 16; State = 1; Message = "Invalid object name 'sys.resource_governor_external_resource_pools'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Number = 207; Class = 16; State = 1; Message = "Invalid column name 'is_distributed'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Message = "Invalid object name 'sys.federations'." }) { Write-Verbose "$($tab)Skipping undefined federations." } else { throw } } } else { Write-Verbose "$($tab)Recursing through $propertyName as a non-collection" try { $OutputObject = ConvertFrom-DbSmo $propertyValue $OutputObject $Depth "$path/$propertyName" $primaryKeyColumns $ServerName } catch { if (Test-Error Microsoft.SqlServer.Management.Sdk.Sfc.InvalidVersionEnumeratorException) { # e.g. Availability Groups on lower versions of SQL Server Write-Verbose "$($tab)Property collection not valid on this version." } elseif (Test-Error System.UnauthorizedAccessException) { throw "Administrator (or other) permission required to use WMI." } elseif (Test-Error @{ ErrorCode = "InvalidNamespace" }) { throw "SMO is unable to find WMI endpoint; this could be the SMO 2016 -> 2014/2012 bug, SMO 2014 -> 2012 bug, or SQL Server < 2005 (not supported by SMO)." } elseif (Test-Error @{ Number = 926; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's in SUSPECT mode." } elseif (Test-Error @{ Number = 927; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's currently restoring." } elseif (Test-Error @{ Number = 942; Class = 14; State = 4 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's offline." } elseif (Test-Error @{ Number = 945; Class = 14; State = 2 }) { Write-Verbose "$($tab)Unable to examine the database in detail probably because it's part of a mirror/AG and restoring." } elseif (Test-Error @{ Number = 954; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a mirror/AG." } elseif (Test-Error @{ Number = 978; Class = 14; State = 1 }) { Write-Verbose "$($tab)Unable to examine the database in detail because it's part of a AG and has read-intent only." } elseif (Test-Error @{ Number = 3906; Class = 16; State = 1 }) { Write-Verbose "$($tab)Unable to examine this item in detail because the database is read only (often Full-Text catalogs on a secondary in a mirror/AG)." } elseif (Test-Error @{ TargetSite = "System.String GetDbCollation(System.String)" }) { Write-Verbose "$($tab)Likely a database has been set offline but believes it is configured for Auto_Close when it's not. Set the database online, re-disable Auto_Close (even if it's not set), set it back offline. Sometimes the error remains though." } elseif (Test-Error @{ Number = 208; Class = 16; State = 1; Message = "Invalid object name 'sys.resource_governor_external_resource_pools'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Number = 207; Class = 16; State = 1; Message = "Invalid column name 'is_distributed'." }) { Write-Verbose "$($tab)SMO is unable to query some data in preview releases of SQL 2016 prior to RTM." } elseif (Test-Error @{ Message = "Invalid object name 'sys.federations'." }) { Write-Verbose "$($tab)Skipping undefined federations." } else { throw } } } } } catch { throw } # Finished looping properties # We set an exception not to write the row if it's part of the ServerConfiguration collection (as we write them separately) if ($writeRow) { Write-Verbose "$($tab)Writing row for $tableName" try { [void] $table.Rows.Add($row) } catch { # Choke point for exceptions throw } } else { Write-Verbose "$($tab)Not writing row for $tableName" } Write-Verbose "$($tab)Return" $OutputObject } |