New-DbSmoSchema.ps1

<#
 
.SYNOPSIS
 
.DESCRIPTION
 
.PARAMETER
 
.INPUTS
 
.OUTPUTS
 
.EXAMPLE
 
#>


function New-DbSmoSchema {
    [CmdletBinding(DefaultParameterSetName = "ServerInstance")]
    param (
        [Parameter(Mandatory = $true)]
        [System.Data.DataSet] $DataSet,

        [Parameter(Mandatory = $true, ParameterSetName = "ServerInstance")]
        $ServerInstance,
        [Parameter(Mandatory = $true, ParameterSetName = "ServerInstance")]
        $DatabaseName,
        [Parameter(ParameterSetName = "Connection")]
        $Connection,

        $SchemaName,
        [switch] $Script
    )

    $scriptText = New-Object System.Collections.ArrayList

    if ($PSCmdlet.ParameterSetName -ne "Connection") {
        $Connection = New-DbConnection -ServerInstance $ServerInstance -DatabaseName $DatabaseName
    }
    $smo = $Connection | Get-DbSmo

    $sqlDatabase = $smo.Databases[$DatabaseName]
    $newSchema = New-Object Microsoft.SqlServer.Management.Smo.Schema($sqlDatabase, $schemaName)
    $newSchema.Refresh()
    if ($Script) {
        Write-Verbose "Schema scripted"
        [void] $scriptText.Add($newSchema.Script())
    } else {
        if ($newSchema.State -eq "Existing") {
            Write-Verbose "Schema altered"
            $newSchema.Alter()
        } else {
            Write-Verbose "Schema created"
            $newSchema.Create()
        }
    }

    $tables = @{}
    foreach ($table in $DataSet.Tables) {
        try {
            $tableName = $table.TableName
            Write-Verbose "Converting table $tableName"
            $newTable = New-Object Microsoft.SqlServer.Management.Smo.Table($sqlDatabase, $tableName, $SchemaName)
            $newTable.Refresh() # This will fill the schema from the database if it already exists

            # Add temporal table columns if this is SQL 2016 onwards
            if ($newTable.Columns.Count -eq 0 -and $smo.Version.Major -ge 13) {
                Write-Verbose "Adding temporal fields"
                $dataType = New-Object Microsoft.SqlServer.Management.Smo.DataType("DateTime2", 2)
                $fromColumn = New-Object Microsoft.SqlServer.Management.Smo.Column($newTable, "_ValidFrom", $dataType)
                $fromColumn.Nullable = $false # Columns belonging to a system-time period cannot be nullable.
                $fromColumn.IsHidden = $true
                $fromColumn.GeneratedAlwaysType = "AsRowStart"
                $newTable.Columns.Add($fromColumn)

                $toColumn = New-Object Microsoft.SqlServer.Management.Smo.Column($newTable, "_ValidTo", $dataType)
                $toColumn.Nullable = $false # Columns belonging to a system-time period cannot be nullable.
                $toColumn.IsHidden = $true
                $toColumn.GeneratedAlwaysType = "AsRowEnd"
                $newTable.Columns.Add($toColumn)

                $newTable.AddPeriodForSystemTime("_ValidFrom", "_ValidTo", $true) # If you accidentally passed non strings you get a "must provide existing column" error

                $newTable.HistoryTableSchema = $SchemaName
                $newTable.HistoryTableName = "$($tableName)_History"
                $newTable.IsSystemVersioned = $true
            }

            # Iterate columns where the column names aren't already in the table
            $changed = $false
            foreach ($column in ($table.Columns | Where-Object { ($newTable.Columns | Select-Object -ExpandProperty Name) -notcontains $_.ColumnName })) {
                $dataType = switch ($column.DataType.Name) {
                    "Boolean" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::Bit
                    }
                    "Byte[]" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::VarBinary
                    }
                    "Byte" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::TinyInt
                    }
                    "DateTime" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::DateTime2
                    }
                    "Decimal" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::Decimal
                    }
                    "Double" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::Float
                    }
                    "Guid" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::UniqueIdentifier
                    }
                    "Int16" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::SmallInt
                    }
                    "Int32" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::Int
                    }
                    "Int64" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::BigInt
                    }
                    "UInt16" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::SmallInt
                    }
                    "UInt32" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::BigInt # Cluster.RootMemoryReserved; ClusterNode.DrainTarget; ClusterGroup.FailoverThreshold
                    }
                    "UInt64" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::BigInt
                    }
                    "Single" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::Decimal
                    }
                    "String" {
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::NVarChar
                    }
                    default {
                        # This basically defaults everything else so that if we passed something we really
                        # didn't expect then we won't be storing a raw object; it'll be cast to string.
                        [Microsoft.SqlServer.Management.Smo.SqlDataType]::NVarChar
                    }
                }
                Write-Verbose "Adding column $($column.ColumnName) as $dataType"

                if ($dataType -eq "VarBinary" -or $dataType -eq "VarChar" -or $dataType -eq "NVarChar") {
                    if ($column.MaxLength -ne -1) {
                        $dataType = New-Object Microsoft.SqlServer.Management.Smo.DataType($dataType, $column.MaxLength)
                    } else {
                        $dataType = New-Object Microsoft.SqlServer.Management.Smo.DataType("$($dataType)Max")
                    }
                } elseif ($dataType -eq "Decimal") {
                    # The only known uses of Decimal in SMO are for LSN. It defaults to 18, 0 but we need 25, 0.
                    # AvailabilityDatabase.RecoveryLSN
                    # AvailabilityDatabase.TruncationLSN
                    # Database.MirroringFailoverLogSequenceNumber
                    # DatabaseReplicaState.EndOfLogLSN
                    # DatabaseReplicaState.LastCommitLSN
                    # DatabaseReplicaState.LastHardenedLSN
                    # DatabaseReplicaState.LastReceivedLSN
                    # DatabaseReplicaState.LastRedoneLSN
                    # DatabaseReplicaState.LastSentLSN
                    # DatabaseReplicaState.RecoveryLSN
                    # DatabaseReplicaState.TruncationLSN
                    $dataType = New-Object Microsoft.SqlServer.Management.Smo.DataType($dataType, 25, 0)
                } else {
                    $dataType = New-Object Microsoft.SqlServer.Management.Smo.DataType($dataType)
                }

                $newColumn = New-Object Microsoft.SqlServer.Management.Smo.Column($newTable, $column.ColumnName, $dataType)
                $newColumn.Nullable = $column.AllowDbNull
                $newTable.Columns.Add($newColumn)

                $changed = $true
            }
            $tables.Add($tableName, $newTable)

            # If the SMO table has a primary key but the new/existing table doesn't
            if ($table.PrimaryKey) {
                if (!($newTable.Indexes | Where-Object { $_.IndexKeyType -eq "DriPrimaryKey" })) {
                    $primaryKeyName = $table.Constraints | Where-Object { $_ -is [System.Data.UniqueConstraint] -and $_.IsPrimaryKey } | Select-Object -ExpandProperty ConstraintName
                    Write-Verbose "Adding primary key $primaryKeyName"

                    $primaryKey = New-Object Microsoft.SqlServer.Management.Smo.Index($newTable, $primaryKeyName)
                    $primaryKey.IndexType = [Microsoft.SqlServer.Management.Smo.IndexType]::ClusteredIndex
                    $primaryKey.IndexKeyType = [Microsoft.SqlServer.Management.Smo.IndexKeyType]::DriPrimaryKey

                    foreach ($column in $table.PrimaryKey) {
                        $indexColumn = New-Object Microsoft.SqlServer.Management.Smo.IndexedColumn($primaryKey, $column.ColumnName)
                        $primaryKey.IndexedColumns.Add($indexColumn)
                    }

                    $newTable.Indexes.Add($primaryKey)
                }
            } else {
                Write-Warning "$tableName doesn't have a primary key!"
            }

            if ($changed) {
                # You must script out the table, the primary key, and the foreign keys separately
                if ($Script) {
                    Write-Verbose "Table scripted"
                    [void] $scriptText.Add($newTable.Script())
                } else {
                    if ($newTable.State -eq "Existing") {
                        Write-Verbose "Table altered"
                        $newTable.Alter()
                    } else {
                        Write-Verbose "Table created"
                        $newTable.Create()
                    }
                }
            }
        } catch {
            throw
        }
    }

    foreach ($table in $DataSet.Tables) {
        $tableName = $table.TableName
        $newTable = New-Object Microsoft.SqlServer.Management.Smo.Table($sqlDatabase, $tableName, $SchemaName)
        $newTable.Refresh() # This will fill the schema from the database

        foreach ($constraint in ($table.Constraints | Where-Object { $_ -is [System.Data.ForeignKeyConstraint] -and ($newTable.ForeignKeys | Select-Object -ExpandProperty Name) -notcontains $_.ConstraintName })) {
            try {
                $constraintName = $constraint.ConstraintName
                Write-Verbose "Adding foreign key $($tableName).$constraintName"

                $foreignKey = New-Object Microsoft.SqlServer.Management.Smo.ForeignKey($tables[$tableName], $constraintName)
                $foreignKey.ReferencedTable = $constraint.RelatedTable.TableName
                $foreignKey.ReferencedTableSchema = $SchemaName
                $foreignKey.IsChecked = $true
                for ($i = 0; $i -lt $constraint.Columns.Count; $i++) {
                    $foreignKeyColumn = New-Object Microsoft.SqlServer.Management.Smo.ForeignKeyColumn($foreignKey, $constraint.Columns[$i], $constraint.RelatedColumns[$i])
                    $foreignKey.Columns.Add($foreignKeyColumn)
                }

                # SQL 2017 supports on Delete Cascade with Temporal Tables
                if ($smo.Version.Major -ge 14) {
                    $foreignKey.DeleteAction = "Cascade"
                }

                if ($Script) {
                    [void] $scriptText.Add($foreignKey.Script())
                } else {
                    $foreignKey.Create()
                }
            } catch {
                throw
            }
        }
    }

    if ($Script) {
        $scriptText
    }
}