SqlDataTools.psm1

Set-StrictMode -Version Latest

#Region Global Configuration
try {
    $Script:PSData = $null
}
catch {
    throw $_
}
#EndRegion

#Region Enumerations
enum Mode {
    Export
    Import
    Merge
}

enum Encoding {
    ASCII
    BigEndianUnicode
    BigEndianUTF32
    OEM
    Unicode
    UTF7
    UTF8
    UTF8BOM
    UTF8NoBOM
    UTF32
}
#EndRegion

#Region Classes
Class ArgumentCompleterResult {
    #properties

    #Method
    static [System.Management.Automation.CompletionResult[]] GetArgumentCompleterResult([System.Array]$Arguments) {
        #[string]$CommandName = $Arguments[0]
        [string]$ParameterName = $Arguments[1]
        [string]$WordToComplete = $Arguments[2]
        #[System.Management.Automation.Language.CommandAst]$CommandAst = $Arguments[3]
        [Hashtable]$FakeBoundParameters = $Arguments[4]

        $ParameterValueList = $null

        switch ($ParameterName) {
            'DataTemplateName' {
                $TemplateList = [System.Collections.Generic.List[string]]::New()

                if ($FakeBoundParameters.ContainsKey('Mode')) {
                    switch ($FakeBoundParameters['Mode']) {
                        'Export' {
                            $TemplateList.AddRange([string[]]$Script:PSData.Export.Keys)
                        }
                        'Import' {
                            $TemplateList.AddRange([string[]]$Script:PSData.Import.Keys)

                        }
                        'Merge' {
                            $TemplateList.AddRange([string[]]$Script:PSData.Merge.Keys)
                        }
                    }
                } else {
                    $TemplateList.AddRange([string[]]$Script:PSData.Export.Keys)
                    $TemplateList.AddRange([string[]]$Script:PSData.Merge.Keys)
                }

                $ParameterValueList = $TemplateList
            }
            Default {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New('Unknown argument completer.'),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidType,
                    $ParameterName
                )
            }
        }

        $SelectedParameters = $ParameterValueList.where({ $_ -like "$WordToComplete*" }) | Sort-Object

        $CompletionResultList = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::New()

        foreach ($Parameter in $SelectedParameters) {
            if ($Parameter -match '\s|''') {
                $CompletionText = [string]::Format("'{0}'", $($Parameter.Replace("'", "''")))
            } else {
                $CompletionText = $Parameter
            }

            $ListItemText = $Parameter
            $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue
            $ToolTip = $Parameter

            $CompletionResultList.Add([System.Management.Automation.CompletionResult]::New($CompletionText, $ListItemText, $ResultType, $ToolTip))
        }

        return $CompletionResultList
    }
}

Class ExportConfigurationTemplates : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        if ($null -eq $Script:PSData) {
            throw [System.Management.Automation.ErrorRecord]::New(
                [Exception]::New('Template repository not loaded. Use the Use-TemplateRepository function to load template repository.'),
                '1',
                [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                $null
            )
        } elseif ($null -eq $Script:PSData.Export) {
            return @()
        } else {
            return [string[]]$Script:PSData.Export.Keys
        }
    }
}

Class ImportConfigurationTemplates : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        if ($null -eq $Script:PSData) {
            throw [System.Management.Automation.ErrorRecord]::New(
                [Exception]::New('Template repository not loaded. Use the Use-TemplateRepository function to load template repository.'),
                '1',
                [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                $null
            )
        } elseif ($null -eq $Script:PSData.Import) {
            return @()
        } else {
            return [string[]]$Script:PSData.Import.Keys
        }
    }
}

Class MergeConfigurationTemplates : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        if ($null -eq $Script:PSData) {
            throw [System.Management.Automation.ErrorRecord]::New(
                [Exception]::New('Template repository not loaded. Use the Use-TemplateRepository function to load template repository.'),
                '1',
                [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                $null
            )
        } elseif ($null -eq $Script:PSData.Merge) {
            return @()
        } else {
            return [string[]]$Script:PSData.Merge.Keys
        }
    }
}

class ValidatePathExists : System.Management.Automation.ValidateArgumentsAttribute {
    [string]$PathType = 'Any'

    ValidatePathExists([string]$PathType) {
        $this.PathType = $PathType
    }

    [void]Validate([object]$Path, [System.Management.Automation.EngineIntrinsics]$EngineIntrinsics) {
        if([string]::IsNullOrWhiteSpace($Path)) {
            throw [System.ArgumentNullException]::New()
        }

        if(-not (Test-Path -Path $Path -PathType $this.PathType)) {
            switch ($this.PathType) {
                'Container' {
                    throw [System.IO.DirectoryNotFoundException]::New()
                }
                'Leaf' {
                    throw [System.IO.FileNotFoundException]::New()
                }
                Default {
                    throw [System.InvalidOperationException]::New('An unexpected error has occurred.')
                }
            }
        }
    }
}

class ValidatePathIsValid : System.Management.Automation.ValidateArgumentsAttribute {
    [void]Validate([object]$Path, [System.Management.Automation.EngineIntrinsics]$EngineIntrinsics) {
        if([string]::IsNullOrWhiteSpace($Path)) {
            throw [System.ArgumentNullException]::New()
        }

        if ($Path -is [System.IO.DirectoryInfo]) {
            if (-not $(Test-Path -LiteralPath $Path.Parent -PathType Container)) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [System.ArgumentException]::New("Cannot find path '$Path.Directory' because it does not exist."),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $Path
                )
            } elseif (-not $(Test-Path -LiteralPath $Path -PathType Container -IsValid)) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [System.ArgumentException]::New("The path '$Path.Name' is not valid."),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $Path
                )
            }
        }

        if ($Path -is [System.IO.FileInfo]) {
            if (-not $(Test-Path -LiteralPath $Path.Directory -PathType Container)) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [System.ArgumentException]::New("Cannot find path '$Path.Directory' because it does not exist."),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $Path
                )
            } elseif (-not $(Test-Path -LiteralPath $Path -PathType Leaf -IsValid)) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [System.ArgumentException]::New("The path '$Path.Name' is not valid."),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $Path
                )
            }
        }
    }
}

class TextFieldParser {
    [System.IO.StreamReader]$StreamReader
    [Microsoft.VisualBasic.FileIO.TextFieldParser]$TextFieldParser
    [char]$Delimiter
    [switch]$IgnoreQuotes
    [string[]]$Headers
    [int]$CurrentLine = 0
    [int]$TotalLines = 0

    TextFieldParser([string]$Path, [Encoding]$Encoding, [char]$Delimiter, [switch]$IgnoreQuotes) {
        if([string]::IsNullOrWhiteSpace($Path)) {
            throw [System.ArgumentNullException]::New()
        }

        if (-not (Test-Path -Path $Path -PathType Leaf)) {
            throw [System.IO.FileNotFoundException]::New("Cannot find path '$Path' because it does not exist.")
        }

        try {
            $FileStream = [System.IO.FileStream]::New($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)

            $this.StreamReader = [System.IO.StreamReader]::New($FileStream, $Encoding)
            $this.Delimiter = $Delimiter
            $this.IgnoreQuotes = $IgnoreQuotes

            $this.TotalLines = 0

            while ($null -ne $this.StreamReader.ReadLine()) {
                $this.TotalLines++
            }

            [void]$this.StreamReader.BaseStream.Seek(0, [System.IO.SeekOrigin]::Begin)
            $this.StreamReader.DiscardBufferedData()

            $TextEncoding = [System.Text.Encoding]::$Encoding

            switch ($Encoding) {
                'BigEndianUTF32' {
                    $TextEncoding = [System.Text.Encoding]::GetEncoding('utf-32BE')
                }
                'OEM' {
                    $TextEncoding = [System.Text.Encoding]::GetEncoding([System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage)
                }
                'UTF8BOM' {
                    $TextEncoding = [System.Text.Encoding]::UTF8
                }
                'UTF8NoBOM' {
                    $TextEncoding = [System.Text.Encoding]::New([System.Text.UTF8Encoding]::New($false))
                }
                Default {
                    if ([System.Text.Encoding]::$Encoding -is [System.Text.Encoding]) {
                        $TextEncoding = [System.Text.Encoding]::$Encoding
                    } else {
                        throw [System.Management.Automation.ErrorRecord]::New(
                            [Exception]::New("Invalid encoding specified: $Encoding"),
                            '1',
                            [System.Management.Automation.ErrorCategory]::NotImplemented,
                            $Encoding
                        )
                    }
                }
            }

            $this.TextFieldParser = [Microsoft.VisualBasic.FileIO.TextFieldParser]::New($FileStream, $TextEncoding)

            $this.TextFieldParser.SetDelimiters($Delimiter)

            if ($IgnoreQuotes) {
                $this.TextFieldParser.HasFieldsEnclosedInQuotes = $false
            }

            $this.Headers = $this.TextFieldParser.ReadFields()
        }
        catch {
            throw $_
        }
    }

    TextFieldParser([string]$Path, [Encoding]$Encoding, [char]$Delimiter, [switch]$IgnoreQuotes, [string[]]$RequiredColumnNames) {
        if([string]::IsNullOrWhiteSpace($Path)) {
            throw [System.ArgumentNullException]::New()
        }

        if (-not (Test-Path -Path $Path -PathType Leaf)) {
            throw [System.IO.FileNotFoundException]::New("Cannot find path '$Path' because it does not exist.")
        }

        try {
            $FileStream = [System.IO.FileStream]::New($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)

            $this.StreamReader = [System.IO.StreamReader]::New($FileStream, $Encoding)
            $this.Delimiter = $Delimiter
            $this.IgnoreQuotes = $IgnoreQuotes

            $this.TotalLines = 0

            while ($null -ne $this.StreamReader.ReadLine()) {
                $this.TotalLines++
            }

            [void]$this.StreamReader.BaseStream.Seek(0, [System.IO.SeekOrigin]::Begin)
            $this.StreamReader.DiscardBufferedData()

            $TextEncoding = [System.Text.Encoding]::$Encoding

            switch ($Encoding) {
                'BigEndianUTF32' {
                    $TextEncoding = [System.Text.Encoding]::GetEncoding('utf-32BE')
                }
                'OEM' {
                    $TextEncoding = [System.Text.Encoding]::GetEncoding([System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage)
                }
                'UTF8BOM' {
                    $TextEncoding = [System.Text.Encoding]::UTF8
                }
                'UTF8NoBOM' {
                    $TextEncoding = [System.Text.Encoding]::New([System.Text.UTF8Encoding]::New($false))
                }
                Default {
                    if ([System.Text.Encoding]::$Encoding -is [System.Text.Encoding]) {
                        $TextEncoding = [System.Text.Encoding]::$Encoding
                    } else {
                        throw [System.Management.Automation.ErrorRecord]::New(
                            [Exception]::New("Invalid encoding specified: $Encoding"),
                            '1',
                            [System.Management.Automation.ErrorCategory]::NotImplemented,
                            $Encoding
                        )
                    }
                }
            }

            $this.TextFieldParser = [Microsoft.VisualBasic.FileIO.TextFieldParser]::New($FileStream, $TextEncoding)

            $this.TextFieldParser.SetDelimiters($Delimiter)

            if ($IgnoreQuotes) {
                $this.TextFieldParser.HasFieldsEnclosedInQuotes = $false
            }

            $this.Headers = $this.TextFieldParser.ReadFields()

            $ComparObjectParameters = @{
                'ReferenceObject' = $this.Headers
                'DifferenceObject' = $RequiredColumnNames
            }

            $MissingProperties = (Compare-Object @ComparObjectParameters).where({$_.SideIndicator -eq "=>"})

            if ($MissingProperties.Count -gt 0) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New("CSV missing required properties: $([string]::Join(', ', $MissingProperties.InputObject))"),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $MissingProperties
                )
            }
        }
        catch {
            throw $_
        }
    }

    [void] Close() {
        $this.TextFieldParser.Close()
        $this.StreamReader.Close()
        $this.StreamReader.BaseStream.Close()
    }

    [PSCustomObject] ReadLine() {
        $this.CurrentLine = $this.TextFieldParser.LineNumber

        $Fields = $this.TextFieldParser.ReadFields();

        if ($null -eq $Fields) {
            return $null
        }

        $Row = [PSCustomObject]::New()

        for ($i = 0; $i -lt $Fields.Count; $i++) {
            $Row.PSObject.Properties.Add(
                [PSNoteProperty]::new($this.Headers[$i], $Fields[$i])
            )
        }

        return $Row
    }

    [bool] IsValidRow([PSCustomObject]$Row, [psobject[]]$InputColumns) {
        $WarningFormatString = 'In row {0}, column {1} with the value "{2}" is not a valid value.'
        $IsValidRow = $true
        $Match = $false

        foreach ($InputColumn in $InputColumns) {
            switch ($InputColumn.Keys.where({$_ -NotIn @('DataType', 'Name')})) {
                'ValidatePattern' {
                    $Regex = [System.Text.RegularExpressions.Regex]::New($InputColumn.ValidatePattern)
                    $Match = $Regex.IsMatch($Row.$($InputColumn.Name))

                    Continue
                }
                'ValidateRange' {
                    $TypeObject = [System.Type]::GetType($InputColumn.DataType)

                    $ColumnValue = [System.Management.Automation.LanguagePrimitives]::ConvertTo($Row.$($InputColumn.Name), $TypeObject)

                    $Match = $ColumnValue -ge $InputColumn.ValidateRange[0] -and $ColumnValue -le $InputColumn.ValidateRange[1]

                    Continue
                }
                'ValidateSet' {
                    $Match = $Row.$($InputColumn.Name) -In $InputColumn.ValidateSet

                    Continue
                }
                Default {
                    throw [System.Management.Automation.ErrorRecord]::New(
                        [Exception]::New('Unknown validate method.'),
                        '1',
                        [System.Management.Automation.ErrorCategory]::InvalidOperation,
                        $InputColumn
                    )
                }
            }

            if (-not $Match) {
                Write-Warning $([string]::Format($WarningFormatString, $this.CurrentLine, $InputColumn.Name, $Row.$($InputColumn.Name)))

                $IsValidRow = $false
            }
        }

        return $IsValidRow
    }
}

class TransformPath : System.Management.Automation.ArgumentTransformationAttribute {
    [object]Transform([System.Management.Automation.EngineIntrinsics]$EngineIntrinsics, [object]$InputData) {
        if ([System.IO.Path]::IsPathRooted($InputData)) {
            return $InputData
        } else {
            return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PWD.Path, $InputData))
        }

        throw [System.InvalidOperationException]::New('Unexpected error.')
    }
}
#EndRegion

#Region Type Definitions
$TypeDefinition = @'
using System;
namespace SqlDataTools
{
    public class SqlDataTemplate
    {
        public string Mode;
        public string Name;
        public string Description;
    }
}
'@


Add-Type -TypeDefinition $TypeDefinition

Remove-Variable -Name TypeDefinition
#EndRegion


function Export-SqlData {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $false,
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Low',
        DefaultParameterSetName = 'ServerInstance_Template'
    )]

    [OutputType([System.Void])]

    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Template'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Template'
        )]
        [ValidateSet([ExportConfigurationTemplates])]
        [string]$DataTemplateName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Table'
        )]
        [ValidateLength(1, 128)]
        [string]$TableName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [ValidatePathIsValid()]
        [System.IO.FileInfo][TransformPath()]$Path,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Template'
        )]
        [ValidateLength(1, 128)]
        [Alias('SqlServer')]
        [string]$ServerInstance,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Table'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Template'
        )]
        [Microsoft.Data.SqlClient.SqlConnection]$SqlConnection,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Template'
        )]
        [ValidateLength(1, 128)]
        [Alias('Database')]
        [string]$DatabaseName,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [Encoding]$Encoding,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [char]$Delimiter,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [Switch]$Force
    )

    begin {
        $ServerInstanceParameterSets = @('ServerInstance_Table', 'ServerInstance_Template')
        $TemplateParameterSets = @('ServerInstance_Template', 'SqlConnection_Template')

        try {
            if ($PSCmdlet.ParameterSetName -in $TemplateParameterSets) {
                $Configuration = $Script:PSData.Export."$DataTemplateName"

                $CsvObjectParameters = [System.Collections.ArrayList]::New()

                foreach ($Enumerator in $Configuration.OutputObject.Property.GetEnumerator()) {
                    $Hashtable = @{
                        N = $Enumerator.Name
                        E = [scriptblock]::Create($Enumerator.Expression.ToString().SubString(1, $Enumerator.Expression.ToString().Length - 2))
                    }

                    [void]$CsvObjectParameters.Add($Hashtable)
                }

                $SqlDataAdapterCommandSet = $Configuration.SqlDataAdapterCommandSet
            }

            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                $SqlServerInstanceParameters = @{
                    'ServerInstance' = $ServerInstance
                    'DatabaseName' = 'master'
                    'Verbose' = [bool]($PSBoundParameters['Verbose'])
                }

                $SqlConnection = Connect-SqlServerInstance @SqlServerInstanceParameters
            }
        }
        catch {
            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:\SqlConnection) {
                    $SqlConnection.Dispose()
                }
            }

            throw $_
        }
    }

    process {
        try {
            $SqlClientDataSetParameters = @{
                'SqlConnection' = $SqlConnection
                'DataSetName' = 'SqlDataTools'
                'OutputAs' = 'DataTable'
            }

            if ($PSCmdlet.ParameterSetName -in $TemplateParameterSets) {
                $SqlClientDataSetParameters.Add('SqlCommand', $SqlDataAdapterCommandSet.Select.CommandText)
            } else {
                $SqlClientDataSetParameters.Add('SqlCommand', $TableName)
                $SqlClientDataSetParameters.Add('CommandType', [System.Data.CommandType]::TableDirect)
            }

            $DataTable = Get-SqlClientDataSet @SqlClientDataSetParameters

            $ExportCsvParameters = @{
                'Path' = $Path
                'NoTypeInformation' = $true
            }

            if ($PSBoundParameters.ContainsKey('Encoding')) {
                $ExportCsvParameters.Add('Encoding', [string]$Encoding)
            }

            if ($PSBoundParameters.ContainsKey('Delimiter')) {
                $ExportCsvParameters.Add('Delimiter', $Delimiter)
            }

            if ($Force) {
                $ExportCsvParameters.Add('Force', $true)
            } else {
                $ExportCsvParameters.Add('NoClobber', $true)
            }

            if ($PSCmdlet.ShouldProcess($DatabaseName, "Export $DataTemplateName")) {
                if ($PSCmdlet.ParameterSetName -in $TemplateParameterSets) {
                    $DataTable | Select-Object $CsvObjectParameters | Export-Csv @ExportCsvParameters
                } else {
                    $DataTable | Export-Csv @ExportCsvParameters
                }
            }
        }
        catch {
            throw $_
        }
        finally {
            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:SqlConnection) {
                    Disconnect-SqlServerInstance -SqlConnection $SqlConnection
                }
            }
        }
    }

    end {
    }
}

function Get-SqlDataTemplate {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $true,
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]

    [OutputType([SqlDataTools.SqlDataTemplate])]

    param (
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            Position = 0
        )]
        [Mode]$Mode,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            Position = 1
        )]
        [ArgumentCompleter({ [ArgumentCompleterResult]::GetArgumentCompleterResult($Args) })]
        [string]$DataTemplateName

    )

    begin {
        try {
            if ($null -eq $Script:PSData) {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New('Module not initialized.'),
                    '1',
                    [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                    $null
                )
            }

            $Configuration = $Script:PSData
        }
        catch {
            throw
        }
    }

    process {
        $ModeNames = [System.Enum]::GetNames([Mode]) | Sort-Object

        foreach ($ModeName in $ModeNames) {
            if ($PSBoundParameters.ContainsKey('Mode')) {
                if ($ModeName -ne $Mode) {
                    Continue
                }
            }

            $TemplateKeys = $Configuration[$ModeName].Keys | Sort-Object

            foreach ($TemplateKey in $TemplateKeys) {
                if ($PSBoundParameters.ContainsKey('DataTemplateName')) {
                    if ($DataTemplateName -ne $TemplateKey) {
                        Continue
                    }
                }

                $SqlDataTemplate = [SqlDataTools.SqlDataTemplate]::New()

                $SqlDataTemplate.Mode = $ModeName
                $SqlDataTemplate.Name = $TemplateKey
                $SqlDataTemplate.Description = $Configuration[$ModeName][$TemplateKey].Description

                $SqlDataTemplate
            }
        }
    }

    end {
    }
}

function Import-SqlData {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $false,
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'ServerInstance_DataTemplate'
    )]

    [OutputType([SqlClient.DataSetResult])]

    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_DataTemplate'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_DataTemplate'
        )]
        [ValidateSet([ImportConfigurationTemplates])]
        [string]$DataTemplateName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [ValidatePathExists('Leaf')]
        [System.IO.FileInfo][TransformPath()]$Path,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_DataTemplate'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [ValidateLength(1, 128)]
        [Alias('SqlServer')]
        [string]$ServerInstance,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_DataTemplate'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [ValidateLength(1, 128)]
        [Alias('Database')]
        [string]$DatabaseName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_DataTemplate'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Table'
        )]
        [Microsoft.Data.SqlClient.SqlConnection]$SqlConnection,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Table'
        )]
        [ValidateLength(1, 128)]
        [string]$TableName,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [Encoding]$Encoding,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [char]$Delimiter,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [switch]$IgnoreQuotes,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_DataTemplate'
        )]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_DataTemplate'
        )]
        [switch]$SkipValidation,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance_Table'
        )]
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection_Table'
        )]
        [int]$BatchSize = 1000
    )

    begin {
        $ServerInstanceParameterSets = @('ServerInstance_Table', 'ServerInstance_DataTemplate')

        try {
            if ($PSBoundParameters.ContainsKey('DataTemplateName')) {
                $Configuration = $Script:PSData.Import."$DataTemplateName"

                $DataSetTableName = $Configuration.DataTable.TableName

                $PropertyArray = [System.Collections.ArrayList]::New()

                foreach ($Enumerator in $Configuration.InputObject.Property.GetEnumerator()) {
                    $Hashtable = @{
                        N = $Enumerator.Name
                        E = [scriptblock]::Create($Enumerator.Expression.ToString().SubString(1, $Enumerator.Expression.ToString().Length - 2))
                    }

                    [void]$PropertyArray.Add($Hashtable)
                }

                $CsvObjectParameters = @{
                    'Property' = $PropertyArray
                    'Unique' = $Configuration.InputObject.Unique
                }
            } else {
                $DataSetTableName = [System.IO.Path]::GetFileNameWithoutExtension($Path.Name)

                $CsvObjectParameters = @{
                    'Unique' = $false
                }
            }

            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                $SqlServerInstanceParameters = @{
                    'ServerInstance' = $ServerInstance
                    'DatabaseName' = 'master'
                    'Verbose' = [bool]($PSBoundParameters['Verbose'])
                }

                $SqlConnection = Connect-SqlServerInstance @SqlServerInstanceParameters
            }

            $ProgressParameters = @{
                'Id' = 0
                'Activity' = 'Import SQL Data.'
                'Status' = [string]::Format('Step {0} of {1}', 0, 3)
                'CurrentOperation' = ''
                'PercentComplete' = 0
            }

            $ChildProgressParameters = @{
                'Id' = 1
                'ParentId' = 0
                'Activity' = 'Reading delimited file.'
                'Status' = [string]::Format('Line {0} of {1}', 0, 1)
                'CurrentOperation' = ''
                'PercentComplete' = 0
            }
        }
        catch {
            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:\SqlConnection) {
                    $SqlConnection.Dispose()
                }
            }

            throw $_
        }
    }

    process {
        try {
            #Region Create Dataset
            $ProgressParameters.Activity = 'Import SQL Data.'
            $ProgressParameters.Status = [string]::Format('Step {0} of {1}', 1, 2)
            $ProgressParameters.CurrentOperation = 'Create dataset.'
            $ProgressParameters.PercentComplete = 0 / 2 * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            $DataSet = [System.Data.DataSet]::New('SqlDataTools')

            $DataTable = [System.Data.DataTable]::New($DataSetTableName)

            if ($PSBoundParameters.ContainsKey('DataTemplateName')) {
                $TableName = $Configuration.DataTable.TableName

                foreach ($Column in $Configuration.DataTable.Columns) {
                    $DataColumn = [System.Data.DataColumn]::New($Column.Name, [System.Type]::GetType($Column.DataType))

                    $DataColumn.AllowDBNull = $Column.AllowDBNull

                    if ($Column.Unique -eq 'True') {
                        $DataColumn.Unique = $true
                    }

                    if ([System.Type]::GetType($Column.DataType) -is [System.String]) {
                        $DataColumn.MaxLength = $Column.MaxLength
                    }

                    [void]$DataTable.Columns.Add($DataColumn)
                }

                $KeyColumns = [System.Collections.Generic.List[System.Data.DataColumn]]::New()

                foreach ($PrimaryKey in $Configuration.DataTable.PrimaryKeys) {
                    $KeyColumns.Add($DataTable.Columns[$PrimaryKey])
                }

                try {
                    $DataTable.PrimaryKey = $KeyColumns
                }
                catch {
                    $ErrorMessage = [string]::Format("These columns do not have unique values in table {0}: {1}", $DataSetTableName, [string]::Join(', ', $KeyColumns.ColumnName))

                    throw [System.Management.Automation.ErrorRecord]::New(
                        [Exception]::New($ErrorMessage),
                        '1',
                        [System.Management.Automation.ErrorCategory]::InvalidData,
                        $DataSetTableName
                    )
                }
            }

            $DataSet.Tables.Add($DataTable)
            #Endregion

            #Region Setup Bulk Copy Base Parameters
            $SqlClientBulkCopyBaseParameters = @{
                'SqlConnection' = $SqlConnection
                'TableName' = $TableName
                'QueryTimeout' = 0
                'Verbose' = [bool]($PSBoundParameters['Verbose'])
            }

            if ($PSBoundParameters.ContainsKey('BatchSize')) {
                $SqlClientBulkCopyBaseParameters.Add('BatchSize', $BatchSize)
            } elseif ($Configuration.keys -contains 'BatchSize') {
                $SqlClientBulkCopyBaseParameters.Add('BatchSize', $Configuration.BatchSize)
            }

            if ($Configuration.keys -contains 'EnableStreaming') {
                $SqlClientBulkCopyBaseParameters.Add('EnableStreaming', $Configuration.EnableStreaming)
            }

            if ($Configuration.keys -contains 'NotifyAfter') {
                $SqlClientBulkCopyBaseParameters.Add('NotifyAfter', $Configuration.NotifyAfter)
            }

            if ($Configuration.keys -contains 'SqlBulkCopyOptions') {
                $SqlClientBulkCopyBaseParameters.Add('SqlBulkCopyOptions', $Configuration.SqlBulkCopyOptions)
            }

            $ColumnMappingCollection = [System.Collections.Generic.List[Microsoft.Data.SqlClient.SqlBulkCopyColumnMapping]]::New()

            if ($Configuration.keys -contains 'ColumnMappings') {
                foreach ($ColumnMapping in $Configuration.ColumnMappings) {
                    $SqlClientBulkCopyColumnMapping = [Microsoft.Data.SqlClient.SqlBulkCopyColumnMapping]::New($ColumnMapping.SourceColumn, $ColumnMapping.DestinationColumn)

                    $ColumnMappingCollection.Add($SqlClientBulkCopyColumnMapping)
                }

                if ($ColumnMappingCollection.Count -gt 0) {
                    $SqlClientBulkCopyBaseParameters.Add('ColumnMappingCollection', $ColumnMappingCollection)
                }
            }
            #Endregion

            #Region Add CSV data to dataset
            $ProgressParameters.Activity = 'Import SQL Data.'
            $ProgressParameters.Status = [string]::Format('Step {0} of {1}', 2, 2)
            $ProgressParameters.CurrentOperation = 'Adding CSV data to dataset.'
            $ProgressParameters.PercentComplete = 1 / 2 * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            if ($PSBoundParameters.ContainsKey('Delimiter')) {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, $Delimiter, $IgnoreQuotes)
            } else {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, ',', $IgnoreQuotes)
            }

            if (-not $PSBoundParameters.ContainsKey('DataTemplateName')) {
                foreach ($Column in $TextFieldParser.Headers) {
                    [void]$DataTable.Columns.Add($Column.Trim())
                }
            }

            try {
                $LinesPerGB = [Math]::Ceiling(1gb / ($Path.length / $TextFieldParser.TotalLines))

                $ChildProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
                $ChildProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
                $ChildProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
                $ChildProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

                Write-Verbose $ChildProgressParameters.CurrentOperation
                Write-Progress @ChildProgressParameters

                while (-not $TextFieldParser.TextFieldParser.EndOfData) {
                    $Row = $TextFieldParser.ReadLine()

                    if ($TextFieldParser.TotalLines -lt 10000 -or $TextFieldParser.CurrentLine % [System.Math]::Floor($TextFieldParser.TotalLines / 10000) -eq 0) {
                        $ChildProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
                        $ChildProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
                        $ChildProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
                        $ChildProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

                        Write-Verbose $ChildProgressParameters.CurrentOperation
                        Write-Progress @ChildProgressParameters
                    }

                    if ($SkipValidation -or $TextFieldParser.IsValidRow($Row, $Configuration.InputColumns)) {
                        $TransformedRow = $Row | Select-Object @CsvObjectParameters

                        $DataRow = $DataSet.Tables[$DataSetTableName].NewRow()

                        foreach ($Property in $TransformedRow.PsObject.Properties) {
                            if ($null -eq $Property.Value) {
                                $DataRow.Item($Property.Name) = [DBNull]::Value
                            } else {
                                $DataRow.Item($Property.Name) = $Property.Value
                            }
                        }

                        [void]$DataSet.Tables[$DataSetTableName].Rows.Add($DataRow)

                        if ($TextFieldParser.CurrentLine % $LinesPerGB -eq 0 -or $TextFieldParser.TextFieldParser.EndOfData) {
                            $SqlClientBulkCopyParameters = $SqlClientBulkCopyBaseParameters.PSObject.Copy()

                            $SqlClientBulkCopyParameters.Add('DataTable', $DataSet.Tables[$DataSetTableName])

                            if ($PSCmdlet.ShouldProcess($DatabaseName, "Bulk copy")) {
                                Invoke-SqlClientBulkCopy @SqlClientBulkCopyParameters
                            }

                            $DataSet.Tables[$DataSetTableName].Clear()
                        }
                    }
                }
            }
            catch {
                throw $_
            }
            finally {
                Write-Progress -Id 1 -Activity 'Reading delimited file.' -Completed
            }
            #Endregion
        }
        catch {
            throw $_
        }
        finally{
            Write-Progress -Id 1 -Activity 'Copy SQL Data.' -Completed
            Write-Progress -Id 0 -Activity 'Copy SQL Data.' -Completed

            if (Test-Path -Path Variable:TextFieldParser) {
                $TextFieldParser.Close()
            }

            if (Test-Path -Path Variable:SqlDataAdapter) {
                $SqlDataAdapter.Dispose()
            }

            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:SqlConnection) {
                    Disconnect-SqlServerInstance -SqlConnection $SqlConnection
                }
            }
        }
    }

    end {
    }
}

function Merge-SqlData {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $false,
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High',
        DefaultParameterSetName = 'ServerInstance'
    )]

    [OutputType([SqlClient.DataSetResult])]

    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [ValidateSet([MergeConfigurationTemplates])]
        [string]$DataTemplateName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [ValidatePathExists('Leaf')]
        [System.IO.FileInfo][TransformPath()]$Path,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance'
        )]
        [ValidateLength(1, 128)]
        [Alias('SqlServer')]
        [string]$ServerInstance,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'ServerInstance'
        )]
        [ValidateLength(1, 128)]
        [Alias('Database')]
        [string]$DatabaseName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            ParameterSetName = 'SqlConnection'
        )]
        [Microsoft.Data.SqlClient.SqlConnection]$SqlConnection,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [Encoding]$Encoding,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [char]$Delimiter,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [switch]$IgnoreQuotes,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [switch]$SkipValidation,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [int]$BatchSize = 1000
    )

    begin {
        $ServerInstanceParameterSets = @('ServerInstance')

        try {
            $Configuration = $Script:PSData.Merge."$DataTemplateName"

            $DataSetTableName = $Configuration.DataTable.TableName

            $PropertyArray = [System.Collections.ArrayList]::New()

            foreach ($Enumerator in $Configuration.InputObject.Property.GetEnumerator()) {
                $Hashtable = @{
                    N = $Enumerator.Name
                    E = [scriptblock]::Create($Enumerator.Expression.ToString().SubString(1, $Enumerator.Expression.ToString().Length - 2))
                }

                [void]$PropertyArray.Add($Hashtable)
            }

            $CsvObjectParameters = @{
                'Property' = $PropertyArray
                'Unique' = $Configuration.InputObject.Unique
            }

            $SqlDataAdapterCommandSet = $Configuration.SqlDataAdapterCommandSet

            if ($SqlDataAdapterCommandSet.Keys -NotContains 'Insert' -and $SqlDataAdapterCommandSet.Keys -NotContains 'Update') {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New("Configuration $DataTemplateName not implemented for import."),
                    '1',
                    [System.Management.Automation.ErrorCategory]::NotImplemented,
                    $DataTemplateName
                )
            }

            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                $SqlServerInstanceParameters = @{
                    'ServerInstance' = $ServerInstance
                    'DatabaseName' = 'master'
                    'Verbose' = [bool]($PSBoundParameters['Verbose'])
                }

                $SqlConnection = Connect-SqlServerInstance @SqlServerInstanceParameters
            }

            $ProgressParameters = @{
                'Id' = 0
                'Activity' = 'Import SQL Data.'
                'Status' = [string]::Format('Step {0} of {1}', 0, 3)
                'CurrentOperation' = ''
                'PercentComplete' = 0
            }

            $ChildProgressParameters = @{
                'Id' = 1
                'ParentId' = 0
                'Activity' = 'Reading delimited file.'
                'Status' = [string]::Format('Line {0} of {1}', 0, 1)
                'CurrentOperation' = ''
                'PercentComplete' = 0
            }
        }
        catch {
            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:\SqlConnection) {
                    $SqlConnection.Dispose()
                }
            }

            throw $_
        }
    }

    process {
        try {
            #Region Reading Destination Dataset
            $ProgressParameters.Activity = 'Import SQL Data.'
            $ProgressParameters.Status = [string]::Format('Step {0} of {1}', 1, 3)
            $ProgressParameters.CurrentOperation = 'Reading destination dataset.'
            $ProgressParameters.PercentComplete = 0 / 3 * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            $SqlClientDataSetParameters = @{
                'SqlConnection' = $SqlConnection
                'SqlCommand' = $SqlDataAdapterCommandSet.Select.CommandText
                'DataSetName' = 'SqlDataTools'
                'DataTableName' = $DataSetTableName
                'OutputAs' = 'DataSet'
            }

            $DataSet = Get-SqlClientDataSet @SqlClientDataSetParameters

            $KeyColumns = [System.Collections.Generic.List[System.Data.DataColumn]]::New()

            foreach ($PrimaryKey in $Configuration.DataTable.PrimaryKeys) {
                $KeyColumns.Add($DataSet.Tables[$DataSetTableName].Columns[$PrimaryKey])
            }

            try {
                $DataSet.Tables[$DataSetTableName].PrimaryKey = $KeyColumns
            }
            catch {
                $ErrorMessage = [string]::Format("These columns do not have unique values in table {0}: {1}", $DataSetTableName, [string]::Join(', ', $KeyColumns.ColumnName))

                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New($ErrorMessage),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $DataSetTableName
                )
            }
            #Endregion

            #Region Add CSV data to dataset
            $ProgressParameters.Activity = 'Import SQL Data.'
            $ProgressParameters.Status = [string]::Format('Step {0} of {1}', 2, 3)
            $ProgressParameters.CurrentOperation = 'Adding CSV data to dataset.'
            $ProgressParameters.PercentComplete = 1 / 3 * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            if ($PSBoundParameters.ContainsKey('Delimiter')) {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, $Delimiter, $IgnoreQuotes, $Configuration.InputColumns.Name)
            } else {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, ',', $IgnoreQuotes, $Configuration.InputColumns.Name)
            }

            $Results = [SqlClient.DataSetResult]::New()

            if ($SqlDataAdapterCommandSet.Keys -Contains 'Delete') {
                $UnmodifiedList = [System.Collections.Generic.List[psobject]]::New()
            }

            try {
                $ChildProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
                $ChildProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
                $ChildProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
                $ChildProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

                Write-Verbose $ChildProgressParameters.CurrentOperation
                Write-Progress @ChildProgressParameters

                while ($Row = $TextFieldParser.ReadLine()) {
                    if ($TextFieldParser.TotalLines -lt 10000 -or $TextFieldParser.CurrentLine % [System.Math]::Floor($TextFieldParser.TotalLines / 10000) -eq 0) {
                        $ChildProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
                        $ChildProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
                        $ChildProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
                        $ChildProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

                        Write-Verbose $ChildProgressParameters.CurrentOperation
                        Write-Progress @ChildProgressParameters
                    }

                    if ($SkipValidation -or $TextFieldParser.IsValidRow($Row, $Configuration.InputColumns)) {
                        $TransformedRow = $Row | Select-Object @CsvObjectParameters

                        $PrimaryKeyValuesList = [System.Collections.Generic.List[String]]::New()

                        foreach ($PrimaryKey in $Configuration.DataTable.PrimaryKeys) {
                            $PrimaryKeyValuesList.Add($TransformedRow.$PrimaryKey)
                        }

                        $PrimaryKeyValues = $($PrimaryKeyValuesList -as [string[]])

                        if ($DataSet.Tables[$DataSetTableName].Rows.Contains($PrimaryKeyValues)) {
                            $DataRow = $DataSet.Tables[$DataSetTableName].Rows.Find($PrimaryKeyValues)

                            foreach ($Property in $TransformedRow.PsObject.Properties) {
                                if ($Property.Name -NotIn $Configuration.DataTable.PrimaryKeys) {
                                    if ($null -eq $Property.Value) {
                                        if ($DataRow.Item($Property.Name) -ne [DBNull]::Value) {
                                            $DataRow.Item($Property.Name) = [DBNull]::Value
                                        }
                                    } else {
                                        if ($DataRow.Item($Property.Name) -ne $Property.Value) {
                                            $DataRow.Item($Property.Name) = $Property.Value
                                        }
                                    }
                                }
                            }

                            if ($DataRow.RowState -eq 'Unchanged') {
                                if ($SqlDataAdapterCommandSet.Keys -Contains 'Delete') {
                                    $UnmodifiedList.Add($($DataRow | Select-Object -Property $Configuration.DataTable.PrimaryKeys))
                                }

                                $Results.UnchangedRows++
                            } else {
                                $Results.UpdatedRows++
                            }
                        } else {
                            $DataRow = $DataSet.Tables[$DataSetTableName].NewRow()

                            foreach ($Property in $TransformedRow.PsObject.Properties) {
                                if ($null -eq $Property.Value) {
                                    $DataRow.Item($Property.Name) = [DBNull]::Value
                                } else {
                                    $DataRow.Item($Property.Name) = $Property.Value
                                }
                            }

                            [void]$DataSet.Tables[$DataSetTableName].Rows.Add($DataRow)

                            $Results.InsertedRows++
                        }
                    }
                }
            }
            catch {
                throw $_
            }
            finally {
                Write-Progress -Id 1 -Activity 'Reading delimited file.' -Completed
            }
            #Endregion

            #Region Delete Rows from dataset
            if ($SqlDataAdapterCommandSet.Keys -Contains 'Delete') {
                $UnchangedRows = $DataSet.Tables[$DataSetTableName].Rows.where({$_.RowState -eq 'Unchanged'})

                if ($UnchangedRows.Count -gt $UnmodifiedList.Count) {
                    $Difference = Compare-Object -ReferenceObject $($UnchangedRows | Select-Object $Configuration.DataTable.PrimaryKeys) -DifferenceObject $UnmodifiedList

                    foreach ($Item in $Difference) {
                        if ($Item.SideIndicator -eq '<=') {
                            $PrimaryKeyValuesList = [System.Collections.Generic.List[String]]::New()

                            foreach ($PrimaryKey in $Configuration.DataTable.PrimaryKeys) {
                                $PrimaryKeyValuesList.Add($Item.InputObject.$PrimaryKey)
                            }

                            $DataSet.Tables[$DataSetTableName].Rows.Find($($PrimaryKeyValuesList -as [string[]])).Delete()

                            $Results.UnchangedRows--
                            $Results.DeletedRows++
                        }
                    }
                }

                Remove-Variable -Name UnmodifiedList
            }
            #EndRegion

            #Region Set Sql Data Adapter
            $ProgressParameters.Activity = 'Import SQL Data.'
            $ProgressParameters.Status = [string]::Format('Step {0} of {1}', 3, 3)
            $ProgressParameters.CurrentOperation = 'Adding CSV data to dataset.'
            $ProgressParameters.PercentComplete = 2 / 3 * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            $TotalChangedRows = $Results.InsertedRows + $Results.UpdatedRows + $Results.DeletedRows

            if ($TotalChangedRows -gt 0) {
                $SqlDataAdapter = [Microsoft.Data.SqlClient.SqlDataAdapter]::New()

                $SqlDataAdapter.UpdateBatchSize = $BatchSize

                foreach ($Key in $SqlDataAdapterCommandSet.Keys) {
                    $SqlCommand = $SqlDataAdapterCommandSet.$Key

                    $SqlCommandObject = [Microsoft.Data.SqlClient.SqlCommand]::New($SqlCommand.CommandText, $SqlConnection)

                    if ($Key -ne 'Select') {
                        $SqlCommandObject.UpdatedRowSource = [System.Data.UpdateRowSource]::None
                    }

                    if ($SqlCommand.Keys -contains 'Parameters') {
                        foreach ($Parameter in $SqlCommand.Parameters) {
                            [void]$SqlCommandObject.Parameters.Add($Parameter.ParameterName, [System.Data.SqlDbType]$Parameter.SqlDbType)

                            switch ($Parameter.keys) {
                                {$_ -contains 'Precision'} {
                                    $SqlCommandObject.Parameters[$Parameter.ParameterName].Precision = $Parameter.Precision
                                }
                                {$_ -contains 'Scale'} {
                                    $SqlCommandObject.Parameters[$Parameter.ParameterName].Scale = $Parameter.Scale
                                }
                                {$_ -contains 'Size'} {
                                    $SqlCommandObject.Parameters[$Parameter.ParameterName].Size = $Parameter.Size
                                }
                                {$_ -contains 'SourceColumn'} {
                                    $SqlCommandObject.Parameters[$Parameter.ParameterName].SourceColumn = $Parameter.SourceColumn
                                }
                                {$_ -contains 'SourceVersion'} {
                                    $SqlCommandObject.Parameters[$Parameter.ParameterName].SourceVersion = [System.Data.DataRowVersion]$Parameter.SourceVersion
                                }
                                Default {}
                            }
                        }
                    }

                    switch ($Key) {
                        'Select' {
                            $SqlDataAdapter.SelectCommand = $SqlCommandObject
                        }
                        'Insert' {
                            $SqlDataAdapter.InsertCommand = $SqlCommandObject
                        }
                        'Update' {
                            $SqlDataAdapter.UpdateCommand = $SqlCommandObject
                        }
                        'Delete' {
                            $SqlDataAdapter.DeleteCommand = $SqlCommandObject
                        }
                    }
                }

                if ($PSCmdlet.ShouldProcess($DatabaseName, "Update $DataTemplateName")) {
                    $SqlClientDataSetParameters = @{
                        'DataSet' = $DataSet
                        'DataSetTableName' = $DataSetTableName
                        'SqlDataAdapter' = $SqlDataAdapter
                        'Confirm' = $false
                    }

                    $Results = Save-SqlClientDataSet @SqlClientDataSetParameters
                }
            }
            #Endregion

            $Results
        }
        catch {
            throw $_
        }
        finally{
            Write-Progress -Id 1 -Activity 'Import SQL Data.' -Completed
            Write-Progress -Id 0 -Activity 'Import SQL Data.' -Completed

            if (Test-Path -Path Variable:TextFieldParser) {
                $TextFieldParser.Close()
            }

            if (Test-Path -Path Variable:SqlDataAdapter) {
                $SqlDataAdapter.Dispose()
            }

            if (Test-Path -Path Variable:SqlCommandObject) {
                $SqlCommandObject.Dispose()
            }

            if ($PSCmdlet.ParameterSetName -in $ServerInstanceParameterSets) {
                if (Test-Path -Path Variable:SqlConnection) {
                    Disconnect-SqlServerInstance -SqlConnection $SqlConnection
                }
            }
        }
    }

    end {
    }
}

function Test-SqlData {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $false,
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]

    [OutputType([System.Boolean])]

    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            Position = 0
        )]
        [Mode]$Mode,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            Position = 1
        )]
        [ArgumentCompleter({ [ArgumentCompleterResult]::GetArgumentCompleterResult($Args) })]
        [string]$DataTemplateName,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false,
            Position = 2
        )]
        [ValidatePathExists('Leaf')]
        [System.IO.FileInfo][TransformPath()]$Path,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [Encoding]$Encoding,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [char]$Delimiter,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [switch]$IgnoreQuotes
    )

    begin {
        try {
            if ($Mode -eq 'Export') {
                throw [System.Management.Automation.ErrorRecord]::New(
                    [Exception]::New('Export mode not valid for test.'),
                    '1',
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $Mode
                )
            }

            $Configuration = $Script:PSData."$Mode"."$DataTemplateName"

            $ProgressParameters = @{
                'Id' = 0
                'Activity' = 'Reading delimited file.'
                'Status' = [string]::Format('Line {0} of {1}', 0, 1)
                'CurrentOperation' = ''
                'PercentComplete' = 0
            }
        }
        catch {
            throw
        }
    }

    process {
        try {
            if ($PSBoundParameters.ContainsKey('Delimiter')) {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, $Delimiter, $IgnoreQuotes, $Configuration.InputColumns.Name)
            } else {
                $TextFieldParser = [TextFieldParser]::New($Path, $Encoding, ',', $IgnoreQuotes, $Configuration.InputColumns.Name)
            }

            $ReturnValue = $true

            $ProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
            $ProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
            $ProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
            $ProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

            Write-Verbose $ProgressParameters.CurrentOperation
            Write-Progress @ProgressParameters

            while ($Row = $TextFieldParser.ReadLine()) {
                if ($TextFieldParser.TotalLines -lt 10000 -or $TextFieldParser.CurrentLine % [System.Math]::Floor($TextFieldParser.TotalLines / 10000) -eq 0) {
                    $ProgressParameters.Activity = [string]::Format('Line Number: {0}', $TextFieldParser.CurrentLine)
                    $ProgressParameters.Status = [string]::Format('Line {0} of {1}', $TextFieldParser.CurrentLine, $TextFieldParser.TotalLines)
                    $ProgressParameters.CurrentOperation = [string]::Format('Reading line: {0}', $TextFieldParser.CurrentLine)
                    $ProgressParameters.PercentComplete = $TextFieldParser.CurrentLine / $TextFieldParser.TotalLines * 100

                    Write-Verbose $ProgressParameters.CurrentOperation
                    Write-Progress @ProgressParameters
                }

                if (-not $TextFieldParser.IsValidRow($Row, $Configuration.InputColumns)) {
                    $ReturnValue = $false
                }
            }
        }
        catch {
            $ReturnValue = $false

            $PSCmdlet.WriteError($_)
        }
        finally {
            Write-Progress -Id 0 -Activity 'Reading delimited file.' -Completed

            if (Test-Path -Path Variable:TextFieldParser) {
                $TextFieldParser.Close()
            }

            $ReturnValue
        }
    }

    end {
    }
}

function Use-TemplateRepository {
    <#
    .EXTERNALHELP
    SqlDataTools-Help.xml
    #>


    [System.Diagnostics.DebuggerStepThrough()]

    [CmdletBinding(
        PositionalBinding = $false,
        SupportsShouldProcess = $false,
        ConfirmImpact = 'Low'
    )]

    [OutputType([System.Void])]

    param (
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [ValidatePathExists('Leaf')]
        [System.IO.FileInfo][TransformPath()]$DataTemplatePath,

        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $false,
            ValueFromPipelineByPropertyName = $false
        )]
        [switch]$SkipSignatureValidation
    )

    begin {

    }

    process {
        try {
            if (-not $SkipSignatureValidation) {
                $AuthenticodeSignature = Get-AuthenticodeSignature -FilePath $DataTemplatePath

                if ($AuthenticodeSignature.Status -ne 'Valid') {
                    throw [System.Management.Automation.ErrorRecord]::New(
                        [Exception]::New('Invalid signature file on data file.'),
                        '1',
                        [System.Management.Automation.ErrorCategory]::ResourceExists,
                        $DataTemplatePath
                    )
                }
            }

            $PowerShellDataFileParameters = @{
                'LiteralPath' = $DataTemplatePath
            }

            if ($PSVersionTable.PSVersion -ge [version]'7.2') {
                $PowerShellDataFileParameters.Add('SkipLimitCheck', $true)
            }

            # $Script:PSData.Add($(Import-PowerShellDataFile @PowerShellDataFileParameters))
            $Script:PSData = Import-PowerShellDataFile @PowerShellDataFileParameters
        }
        catch {
            throw $_
        }
    }

    end {
    }
}

# SIG # Begin signature block
# MIInywYJKoZIhvcNAQcCoIInvDCCJ7gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDcEblfSYf0dU/0
# qn5kuywlH0uwFkFwEpoovT2o0uZxOKCCINswggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg
# MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB
# cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p
# vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe
# HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd
# gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU
# jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR
# VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS
# LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV
# NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL
# 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd
# SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU
# eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw
# DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/
# T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ
# E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r
# EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y
# 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx
# dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t
# y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy
# tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB
# YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud
# /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS
# uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP
# ubdcMIIGuTCCBKGgAwIBAgIRAJmjgAomVTtlq9xuhKaz6jkwDQYJKoZIhvcNAQEM
# BQAwgYAxCzAJBgNVBAYTAlBMMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2ll
# cyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx
# JDAiBgNVBAMTG0NlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EgMjAeFw0yMTA1MTkw
# NTMyMThaFw0zNjA1MTgwNTMyMThaMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0j
# zwQwIzvBRiznM3M+Y116dbq+XE26vest+L7k5n5TeJkgH4Cyk74IL9uP61olRsxs
# U/WBAElTMNQI/HsE0uCJ3VPLO1UufnY0qDHG7yCnJOvoSNbIbMpT+Cci75scCx7U
# sKK1fcJo4TXetu4du2vEXa09Tx/bndCBfp47zJNsamzUyD7J1rcNxOw5g6FJg0Im
# Iv7nCeNn3B6gZG28WAwe0mDqLrvU49chyKIc7gvCjan3GH+2eP4mYJASflBTQ3HO
# s6JGdriSMVoD1lzBJobtYDF4L/GhlLEXWgrVQ9m0pW37KuwYqpY42grp/kSYE4BU
# QrbLgBMNKRvfhQPskDfZ/5GbTCyvlqPN+0OEDmYGKlVkOMenDO/xtMrMINRJS5SY
# +jWCi8PRHAVxO0xdx8m2bWL4/ZQ1dp0/JhUpHEpABMc3eKax8GI1F03mSJVV6o/n
# mmKqDE6TK34eTAgDiBuZJzeEPyR7rq30yOVw2DvetlmWssewAhX+cnSaaBKMEj9O
# 2GgYkPJ16Q5Da1APYO6n/6wpCm1qUOW6Ln1J6tVImDyAB5Xs3+JriasaiJ7P5KpX
# eiVV/HIsW3ej85A6cGaOEpQA2gotiUqZSkoQUjQ9+hPxDVb/Lqz0tMjp6RuLSKAR
# sVQgETwoNQZ8jCeKwSQHDkpwFndfCceZ/OfCUqjxAgMBAAGjggFVMIIBUTAPBgNV
# HRMBAf8EBTADAQH/MB0GA1UdDgQWBBTddF1MANt7n6B0yrFu9zzAMsBwzTAfBgNV
# HSMEGDAWgBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwEwYD
# VR0lBAwwCgYIKwYBBQUHAwMwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL2NybC5j
# ZXJ0dW0ucGwvY3RuY2EyLmNybDBsBggrBgEFBQcBAQRgMF4wKAYIKwYBBQUHMAGG
# HGh0dHA6Ly9zdWJjYS5vY3NwLWNlcnR1bS5jb20wMgYIKwYBBQUHMAKGJmh0dHA6
# Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdG5jYTIuY2VyMDkGA1UdIAQyMDAwLgYE
# VR0gADAmMCQGCCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJ
# KoZIhvcNAQEMBQADggIBAHWIWA/lj1AomlOfEOxD/PQ7bcmahmJ9l0Q4SZC+j/v0
# 9CD2csX8Yl7pmJQETIMEcy0VErSZePdC/eAvSxhd7488x/Cat4ke+AUZZDtfCd8y
# HZgikGuS8mePCHyAiU2VSXgoQ1MrkMuqxg8S1FALDtHqnizYS1bIMOv8znyJjZQE
# Sp9RT+6NH024/IqTRsRwSLrYkbFq4VjNn/KV3Xd8dpmyQiirZdrONoPSlCRxCIi5
# 4vQcqKiFLpeBm5S0IoDtLoIe21kSw5tAnWPazS6sgN2oXvFpcVVpMcq0C4x/CLSN
# e0XckmmGsl9z4UUguAJtf+5gE8GVsEg/ge3jHGTYaZ/MyfujE8hOmKBAUkVa7NMx
# RSB1EdPFpNIpEn/pSHuSL+kWN/2xQBJaDFPr1AX0qLgkXmcEi6PFnaw5T17UdIIn
# A58rTu3mefNuzUtse4AgYmxEmJDodf8NbVcU6VdjWtz0e58WFZT7tST6EWQmx/Oo
# HPelE77lojq7lpsjhDCzhhp4kfsfszxf9g2hoCtltXhCX6NqsqwTT7xe8LgMkH4h
# Vy8L1h2pqGLT2aNCx7h/F95/QvsTeGGjY7dssMzq/rSshFQKLZ8lPb8hFTmiGDJN
# yHga5hZ59IGynk08mHhBFM/0MLeBzlAQq1utNjQprztZ5vv/NJy8ua9AGbwkMWkO
# MIIG4DCCBMigAwIBAgIQQ7s0QZ8qUnHOP66HyvajHjANBgkqhkiG9w0BAQsFADBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwHhcNMjYwOTEy
# MjEzNTQ3WhcNMjcwOTEyMjEzNTQ2WjCBhTELMAkGA1UEBhMCVVMxFzAVBgNVBAgM
# DlNvdXRoIENhcm9saW5hMREwDwYDVQQHDAhOZXdiZXJyeTEeMBwGA1UECgwVT3Bl
# biBTb3VyY2UgRGV2ZWxvcGVyMSowKAYDVQQDDCFPcGVuIFNvdXJjZSBEZXZlbG9w
# ZXIgUm9iZXJ0IEVkZXIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6
# 72ybkWUB620cgb49nkhI4VvtWKABeSD8277f0Jww+/5fKUxoX2vC77QjmaSXU+I9
# LB2YTj8yCSTDVlxIwJoS7KgsIN9P2EC6ehJe9FRI0n2Rt33/VlRY8kuSh2XsIVGg
# i6Y3RooWVjVmYCAOXuV5zrqjGY1P72nrSIekxcGCTo1Y0nmRwsECCIYn7b3ZM065
# u9b3AtYf3oI4grblWhtY0kbuHNCSeDjpp+qExQgeIs6OpFACJSDASFiDnF8+L+bB
# V+UtUcFbvlu0WpQyblXw9vdN7BEIdqZ/1fxyhjHxqpSuOoTqoZcyDjilXtnMOfWp
# gfgdKPFRc6NHwrmxUyNLAYHOsqBC+bMxamurB1qCJ/16lFbW/YWJRrJsaeA2WaPw
# 8ulkUnZUoP9JgyVE1nwZbhZOgE3YwlVLmBBsAKsRiyJWBYqG1VdaMpfzLYJVNTc8
# 4F/90uC2BvIoafcFfNyc8dIpdd7Ni17JmYuD0+/u1gqUi3p+Qdlk/y+Vgsb1x+0z
# kaMA6CzjFA83szdzwRLFDDnaUOVngyRR8+JLnSNuEw9nNM3G5WWRGBDOkLbwq+NO
# H1gWWAaJJYOlTARrg7ea2JHLl66CBJaaledp2u8hBl1Y5yQTpIkVYx0VPt1Oa7Nq
# kOkXlNjmmlYsZm5pe3fSTBPx5YdUOeIl1sAWXGH/+QIDAQABo4IBeDCCAXQwDAYD
# VR0TAQH/BAIwADA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY2NzY2EyMDIxLmNy
# bC5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNybDBzBggrBgEFBQcBAQRnMGUwLAYIKwYB
# BQUHMAGGIGh0dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0dW0uY29tMDUGCCsGAQUF
# BzAChilodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNlcjAf
# BgNVHSMEGDAWgBTddF1MANt7n6B0yrFu9zzAMsBwzTAdBgNVHQ4EFgQUCdHvF5Qw
# xL78QY6vfJNPC89uTWEwSwYDVR0gBEQwQjAIBgZngQwBBAEwNgYLKoRoAYb2dwIF
# AQQwJzAlBggrBgEFBQcCARYZaHR0cHM6Ly93d3cuY2VydHVtLnBsL0NQUzATBgNV
# HSUEDDAKBggrBgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD
# ggIBADGvhUwApFlOiMwxr4muEgH+EK8xGwyw7qZGQZYdHQZV7E6ev+4i0u2ywMbF
# H0XcYpaB4xubprYIGbltJhIXoIM+BmIB6mgzgAtKMJBIWMECnACZKPAWPJ5vp3Xu
# GLCg0gwQGZEKJInwEFLzplH3G5g8hTO8KSLmWLVWoWHTTA3WI4LgTf/XRs3QYqur
# bB1gWRWa+vx8J/4I6znbpnpRDxy/jCYh9qtv21Dk3BovIPnfaj50JOWJhWeongQ6
# Dgd4/FZhM/U1Fj/g1W7WDMal9q43MABwmrHPbxrIEK1V5vXwAhK1m9eSaZ8bqbeA
# SId0wOYzIyEziquoO5TCdf/lSi8nD4BIm2E+h738pLQXWvr6tYWyvqaUN0uk5f27
# NsXlVRYp8EUZPP83BJMaQJFgTsYPMeZejAndk3nuqPVGeCL6WW90M7eK5sPbTAmW
# WrSnYFx4pgnMR3X3s14074ytJ3o3ycKa0bxjhMcoCTfMDmV7jUUhATpW8iZ2/E4b
# +0s7DmbN37VsBngsj04vMyVxhcNLSwdFDTQEgkEwHccChlw0anfoZJ7Xui4x5RSr
# j3rOyyrf7mFYIpsbDYjVxBo5/JVJuu5h2+BRxY9MLoSMknm391tCI7aVC/XTl/zW
# rX1kJeCx0nYBnZnmjWCRrCXWOX5V8QaLAnK/R6durGeXnlo3MIIG7TCCBNWgAwIB
# AgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV
# UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRy
# dXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0ExMB4X
# DTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEyNTYg
# UlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjYgMTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSbTGWz/TjGMPh8CQ6RucZCLv5a
# nHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP0BMxt9l3Dm5oCG5Z69AqEN1k
# gHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3BSkUxDjLJhBI+qiZP3aixAC/
# qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNFKZZEeoy/0B8Zm+nzKBgSWG52
# lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbjbrEOvZ3fP2VxIgeR28Y90ZSt
# d3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26QZYMn/FnbuTegcoRcFEZ9FOqM
# 5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRsIolvykE1jive4PuESaT8zzVe
# FWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/ScpaZCZVl6pNUOCybMc/kxC6E
# AmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5prW83vV8d67WowuUs0HdY4H8
# AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0ODjdzi0xlp70W+ow63/0++YD
# EX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwroczk9icflf55W1zOEKAcJVAIXpc
# MCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQUyWOKMC7U
# SvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezLTjAO
# BgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsGAQUF
# BwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w
# XQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNydDBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5jcmww
# IAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUA
# A4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLaDXQIENoyKqxrFbJblzCVP/pk
# XmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEtrfGhz45F4Wt4WGdNdIbRWt3Y
# TYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAdZkfHhHv1mx9EVh71SJelcEN/
# oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAYyzSA02nXD1nYLI7Ijmiv+xLC
# iYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8pD7KVyF5aQyWP4KhVGUUTnmm
# /RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v5jhtKVnd3D0rbHXEu27o+Q8p
# 4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeSWmvKqzpAec9etE14jOQAzLKW
# dE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR5eHs0NzU/+xX7NbEdcofy0W3
# Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZEbGRsMNbXmMREUTgu+Oca7Y7
# 3MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjWagkJNt68Yfim6cjEzVSiLSeZ
# fdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvdN8yWQPT9gzGCBkYwggZCAgEB
# MGowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMg
# Uy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBDuzRB
# nypScc4/rofK9qMeMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEICIQnhKuAZI4bb9Panyl
# G9zvk/Lzg0HFwkmOX3iUokKXMA0GCSqGSIb3DQEBAQUABIICAENnoIyRzcl6LZDF
# +h2awA5COM5zcJv0Gr1jCisyAna5vJUriDFXbLJ1c2hd+Ie/zHhNbUcpJuaGXhBC
# HTk91cHT5LPJW4fL8JA++OxtnvOJGiR+XSpfbn/8OfsKsokdVvrjsTmJDGK2zUMe
# /skvfjAXJyrOLC+HR3WNucKjvpPQ9KNNbEEU1WF3B776GAxjHyjLc8kCyo5Dn5fu
# gU3FEwfJ/UU0Hq5+bs4TtJmVNUI/aT7suIEfb0hShtksqa+nYL6EblQa55HpwGn8
# iVbaGpWz4+YboNepGiOq8H7bJqDQkbMzz5bNVOfjpq/xPrpSUHQoSi2sX55QrMbC
# TgN39PA5lhJMN1BaIIgCO4X8zP8Ru3v/W4IZ9wJrkSuTyBsYYW9hLHpPlDYTWDth
# Ji2QSaohOnUWl7RNZBnXCYhlmpXJv4c2dxCQYb+/VxXpTx9TSCOV3SazXuuNnS2Y
# zrOsoQ6W+FK9bkeGr4RQnYoG2evpWTDr2seor52wyUzNmEsZnXk0Y4GNTfAwhr33
# f0pe2QlGzNznjAgrBhl86AGZ5pqakMhYNfDf5cT9l1o2p0Tpt8ciIX8LU20dCSpQ
# Bw+O0mRUFRMa5EeMHphFnZ6n2PEd+Q1ibp8JzfJX0Vc762GFC+2NcvWDv9Zf6/GR
# eupWdXH4gM3IYtbIr2NRE05ZrTkLoYIDJjCCAyIGCSqGSIb3DQEJBjGCAxMwggMP
# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw
# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0MDk2
# IFNIQTI1NiAyMDI1IENBMQIQCE/cM09+RU7bww+P+ZIYNTANBglghkgBZQMEAgEF
# AKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI2
# MDkxMzE5MzU1NFowLwYJKoZIhvcNAQkEMSIEIEwI9Du+vs9h6n6mPrgfGeeY9fWv
# 5eGn4E8VDNn9msQtMA0GCSqGSIb3DQEBAQUABIICAHq25eLt2SGG9oehoU3AGGQI
# 5uND4id3c9T6hwsabSyAm1HDPKkQdNYfXH/g58fViaxR7l+l2xop3Am458hi6zj6
# GAf1rlqUtnycTEWYRzBpv0w2WzGkOPm2xu7PnWFmOqK5c+6+Lhd7h/E70yHiXMEz
# 8j+axHPv76BK6BjSn2l9UqS2KZorS3c3hikEIR4AdOGANTos7U/CJhgxXhDsh8cF
# ZIVZ7LMZm9JgYdI0OdRLB/lcRofPNcYfvgU14Z7eQZonr1dqgAsGFpvDuJggLtVj
# 1WsDYxYHnQ/xf8ccvzyIZS2M74P+12WdvCupVGVW9q/Wg9qoWxql4RLqnpJd85qX
# ngNgKtF2pAHWLGHzh82WJmeFRymSNTp1NIonSWD8IEuAY2WjWd9QMqZtWjZA/Joj
# AFTFNasp9yyhIOg/IIWxnC/TQE0Eqo6uQoT9hISibOrVUr2e6872KyFAEe3+WVNy
# EJX+CUB8mSKpTKHynjto/DatvLKTB7xAWy1DNnn4wM+RRhUF/YDmUr7ji9/HHFlG
# Lb2A/D9P0gHN4FXxEAYqWlOmfpMEHqxk1azqNIVvF9AHGnIwUYegUs1UHTtauQd1
# H8wD55vNB14CFH2wwnd1HzaNQrOfG7x9RvRBp0ME6I4qCMuhrziGfmedmulfKN1y
# iAZ/NtMrrlVJTtDqDe9q
# SIG # End signature block