Yodel.psm1


#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

enum Yodel_MsSql_QueryKeyword
{
    Default = 1
}

# Functions should use $moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$moduleRoot = $PSScriptRoot

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = & {
    Join-Path -Path $moduleRoot -ChildPath 'Functions\*.ps1'
    Join-Path -Path $moduleRoot -ChildPath 'Functions\MsSql\*.ps1'
}
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function Connect-YDatabase
{
    <#
    .SYNOPSIS
    Opens an ADO.NET connection to a database.
 
    .DESCRIPTION
    The `Connect-YDatabase` function opens an ADO.NET (i.e. pure .NET) connection to a database. Pass the connection string to the `ConnectionString` parameter. Pass the provider to use to connect to the `Provider` parameter. This parameter should be an instance of a `Data.Common.DbProviderFactory` object. The .NET framework ships with several:
 
    * SQL Server: `Connect-YDatabase -Provider ([Data.SqlClient.SqlClientFactory]::Instance)`
    * ODBC: `Connect-YDatabase -Provider ([Data.Odbc.OdbcFactory]::Instance)`
    * OLE: `Connect-YDatabase -Provider ([Data.OleDb.OleDbFactory]::Instance)`
    * Entity Framework: `Connect-YDatabase -Provider ([Data.EntityClient.EntityProviderFactory]::Instance)`
    * Oracle: `Connect-YDatabase -Provider ([Data.OracleClient.OracleClientFactory]::Instance)`
 
    The function uses each provider to create a connection object, sets that connection's connection string, open the connection, and then returns the connection.
 
    The `Connect-YDatabase` also has a simplified parameter set to open a connection to SQL Server. Pass the SQL Server name (e.g. `HOST\INSTANCE`) to the `SqlServerName` parameter, the database name to the `DatabaseName` parameter, and any other connection properties to the `ConnectionString` property. The function will create a connection to the SQL Server database using integrated authentication. To connect as a specific user, pass that user's credentials to the `Credential` parameter. ADO.NET requires that the credential's password be in read-only mode, so `Connect-YDatabase` will call `$Credential.Password.MakeReadOnly()`.
 
    Returns a `Data.Common.DbConnection` object, the base class for all ADO.NET connections. You are responsible for closing the connection:
 
        $conn = Connect-YDatabase -SqlServerName '.' -DatabaseName 'master'
        try
        {
            # run some queries
        }
        finally
        {
            # YOU MUST DO THIS!
            $conn.Close()
        }
 
    .EXAMPLE
    Connect-YDatabase -SqlServerName '.' -DatabaseName 'master'
 
    Demonstrates how to connect to Microsoft SQL Server using integrated authentiction.
 
    .EXAMPLE
    Connect-YDatabase -SqlServerName '.' -DatabaseName 'master' -Credential $credential
 
    Demonstrates how to connect to Microsoft SQL Server as a specific user. The `$credential` parameter must be `PSCredential` object.
 
    .EXAMPLE
    Connect-YDatabase -SqlServerName '.' -DatabaseName 'master' -ConnectionString 'Application Name=Yodel;Workstation ID=SomeComputerName'
 
    Demonstrates how to supply additional connection string properties when using the SQL Server parameter set.
 
    .EXAMPLE
    Connect-YDatabase -Provider ([Data.Odbc.OdbcFactory]::Instance) -ConnectionString 'some connection string'
 
    Demonstrates how to connect to a database using ODBC.
 
    .EXAMPLE
    Connect-YDatabase -Provider ([Data.OleDb.OleDbFactory]::Instance) -ConnectionString 'some connection string'
 
    Demonstrates how to connect to a database using OLE.
 
    .EXAMPLE
    Connect-YDatabase -Provider ([Data.EntityClient.EntityProviderFactory]::Instance) -ConnectionString 'some connection string'
 
    Demonstrates how to connect to a database using the Entity Framework provider.
 
    .EXAMPLE
    Connect-YDatabase -Provider ([[Data.OracleClient.OracleClientFactory]::Instance) -ConnectionString 'some connection string'
 
    Demonstrates how to connect to a database using Oracle.
    #>

    [CmdletBinding()]
    [OutputType([Data.Common.DbConnection])]
    param(
        [Parameter(Mandatory, ParameterSetName='SqlServer')]
        [String]$SqlServerName,

        [Parameter(Mandatory, ParameterSetName='SqlServer')]
        [String]$DatabaseName,

        [Parameter(ParameterSetName='SqlServer')]
        [pscredential]$Credential,

        [Parameter(Mandatory, ParameterSetName='Generic')]
        [Data.Common.DbProviderFactory] $Provider,

        # The connection string to use.
        [String]$ConnectionString,

        # The connection timeout. By default, uses the .NET default of 30 seconds. If it takes longer than this number of seconds to connect, the function will fail.
        #
        # Setting this property adds a `Connection Timeout` property to the connection string if you're connecting to a SQL Server database (i.e. using the `SqlServerName` parameter). If you're connecting via ODBC, the `ConnectionTimeout` property is set. In all other cases, this parameter is ignored.
        #
        # If you get an error that `ConnectionTimeout` is a read-only property, you'll need to pass the timeout as a property in your connection string.
        [int]$ConnectionTimeout
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $PSCmdlet.ParameterSetName -eq 'SqlServer' )
    {
        $Provider = [Data.SqlClient.SqlClientFactory]::Instance
    }

    $connection = $Provider.CreateConnection()
    $connStringBuilder = $Provider.CreateConnectionStringBuilder()

    if( $ConnectionString )
    {
        # There's some weird PowerShell magic going on when setting the ConnectionString property, so directly call the setter function.
        $connStringBuilder.set_ConnectionString($ConnectionString)
    }

    if( $PSCmdlet.ParameterSetName -eq 'SqlServer' )
    {
        $connStringBuilder['Server'] = $SqlServerName
        $connStringBuilder['Database'] = $DatabaseName

        if( $Credential )
        {
            $Credential.Password.MakeReadOnly()
            $sqlCredential = [Data.SqlClient.SqlCredential]::new($Credential.UserName, $Credential.Password)
            $connection.Credential = $sqlCredential
        }
        else
        {
            $connStringBuilder['Integrated Security'] = 'True'
        }

        if( $ConnectionTimeout )
        {
            $connStringBuilder['Connection Timeout'] = $ConnectionTimeout
        }

        $ConnectionString = $connStringBuilder.ToString()
    }
    else
    {
        if( $ConnectionTimeout )
        {
            $connection.ConnectionTimeout = $ConnectionTimeout
        }
    }

    $connection.ConnectionString = $ConnectionString
    $connection.Open()
    return $connection
}


function Invoke-YDbCommand
{
    <#
    .SYNOPSIS
    Uses ADO.NET to execute a database command.
 
    .DESCRIPTION
    `Invoke-YDbCommand` executes a command against a database and returns a generic object for each row in the result
    set. Each object has a property for each column. If a column doesn't have a name, a generic `ColumnX` name is
    assigned, where `X` starts at 0 and increases by one for each nameless column.
 
    Pass the connection to the database to the `Connection` parameter. (Use the `Connect-YDatabase` function to create a
    connection.) Pass the command to run to the `Text` parameter. You may also pipe commands to `Invoke-YDbCommand`. If
    your command should be part of a transaction, pass the transaction to the `Transaction` parameter.
 
    If your command returns a single value, use the `-AsScalar` switch.
 
    If your command returns no results, use the `-NonQuery` switch. If your command affects any rows, the number of rows
    affected will be returned.
 
    To run a parameterized command, use `@name` parameters in your command and pass the values of those parameters in a
    hashtable to the `Parameter` parameter, e.g. `@{ '@name' = 'the_name' }`.
 
    To execute a stored procedure, set the `Text` parameter to the name of the stored procedure, set the `Type`
    parameter to `[Data.CommandType]::StoredProcedure`, and pass the procedure's parameters to the `Parameter` parameter
    (a hashtable of parameter names and values).
 
    Commands will time out after 30 seconds (the default .NET timeout). If you have a query that runs longer, pass the
    number of seconds to wait to the `Timeout` parameter.
 
    Command timings are output to the verbose stream, including the text of the command. Command parameters are not
    output. If you want to suppress sensitive commands from being output, set the `Verbose` parameter to `$false`, e.g.
    `-Verbose:$false`.
 
    Failed queries do not cause a terminating error. If you want your script to stop if your query fails, set the
    `ErrorAction` parameter to `Stop`, e.g. `-ErrorAction Stop`.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'select * from MyTable'
 
    Demonstrates how to select rows from a table.
 
    .EXAMPLE
    'select 1','select 2' | Invoke-YDbCommand -Connection $conn
 
    Demonstrates that you can pipe commands to `Invoke-YDbCommand`.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'select count(*) from MyTable' -AsScalar
 
    Demonstrates how to return a scalar value. If the command returns multiple rows/columns, returns the first row's
    first column's value.
 
    .EXAMPLE
    $rowsDeleted = Invoke-YDbCommand -Connection $conn -Text 'delete from dbo.Example' -NonQuery
 
    Demonstrates how to execute a command that doesn't return a value. If your command updates/deletes rows, the number
    of rows affected is returned.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'insert into MyTable (Two,Three) values @Column2, @Column3' -Parameter @{ '@Column2' = 'Value2'; '@Column3' = 'Value3' } -NonQuery
 
    Demonstrates how to use parameterized queries.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'sp_addrolemember -CommandType [Data.CommandType]::StoredProcedure -Parameter @{ '@rolename = 'db_owner'; '@membername' = 'myuser'; }
 
    Demonstrates how to execute a stored procedure, including how to pass its parameters using the `Parameter`
    parameter.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'create login [yodeltest] with password ''P@$$w0rd''' -Verbose:$false
 
    Demonstrates how to prevent command timings for sensitive queries from being written to the verbose stream.
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'select * from a_really_involved_join_that_takes_a_long_time' -Timeout 120
 
    Demonstrates how to set the command timeout for commands that take longer than .NET's default timeout (30 seconds).
 
    .EXAMPLE
    Invoke-YDbCommand -Connection $conn -Text 'create table my_table (id int)' -Transaction $transaction
 
    Demonstrates that you can make the command part of a transaction by passing the transaction to the `Transaction`
    parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='ExecuteReader')]
    param(
        # The connection to use.
        [Parameter(Mandatory, Position=0)]
        [Data.Common.DbConnection] $Connection,

        # The command to run/execute.
        [Parameter(Mandatory, Position=1, ValueFromPipeline)]
        [String] $Text,

        # The type of command being run. The default is `Text` for a SQL query.
        [Data.CommandType] $Type = [Data.CommandType]::Text,

        # The time (in seconds) to wait for a command to execute. The default is the .NET default, which is 30 seconds.
        [int] $Timeout,

        [Parameter(Position=2)]
        # Any parameters used in the command.
        [hashtable] $Parameter,

        [Parameter(Mandatory,ParameterSetName='ExecuteScalar')]
        # Return the result as a single value instead of a row. If the command returns multiple rows/columns, the value
        # of the first row's first column is returned.
        [switch] $AsScalar,

        [Parameter(Mandatory,ParameterSetName='ExecuteNonQuery')]
        # Executes a command that doesn't return any records. For updates/deletes, the number of rows affected will be
        # returned unless the NOCOUNT options is used.
        [switch] $NonQuery,

        # Any transaction the command should be part of.
        [Data.Common.DbTransaction] $Transaction
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $verboseEnabled = $false
        if( (Write-Verbose 'Active' 4>&1) )
        {
            $verboseEnabled = $true
        }
    }

    process
    {
        $cmd = $Connection.CreateCommand()
        $cmd.CommandText = $Text
        $cmd.CommandTimeout = $Timeout
        $cmd.CommandType = $Type

        if( $Transaction )
        {
            $cmd.Transaction = $Transaction
        }

        if( $Parameter )
        {
            foreach( $name in $Parameter.Keys )
            {
                $value = $Parameter[$name]
                if( -not $name.StartsWith( '@' ) )
                {
                    $name = '@{0}' -f $name
                }
                [void]$cmd.Parameters.AddWithValue( $name, $value )
            }
        }

        $stopwatch = [Diagnostics.Stopwatch]::StartNew()
        try
        {
            if( $pscmdlet.ParameterSetName -like 'ExecuteNonQuery*' )
            {
                $rowsAffected = $cmd.ExecuteNonQuery()
                if( $rowsAffected -ge 0 )
                {
                    $rowsAffected
                }
            }
            elseif( $pscmdlet.ParameterSetName -like 'ExecuteScalar*' )
            {
                $cmd.ExecuteScalar()
            }
            else
            {
                $cmdReader = $cmd.ExecuteReader()
                try
                {
                    if( $cmdReader.HasRows )
                    {
                        while( $cmdReader.Read() )
                        {
                            $row = @{ }
                            for ($i= 0; $i -lt $cmdReader.FieldCount; $i++)
                            {
                                $name = $cmdReader.GetName( $i )
                                if( -not $name )
                                {
                                    $name = 'Column{0}' -f $i
                                }
                                $value = $cmdReader.GetValue($i)
                                if( $cmdReader.IsDBNull($i) )
                                {
                                    $value = $null
                                }
                                $row[$name] = $value
                            }
                            New-Object 'PsObject' -Property $row
                        }
                    }
                }
                finally
                {
                    $cmdReader.Close()
                }
            }
        }
        catch
        {
            # SQL Server exceptions can be brutally nested.
            $ex = $_.Exception
            while( $ex.InnerException )
            {
                $ex = $ex.InnerException
            }

            $errorMsg = '{0}{1}{2}' -f $_.Exception.Message,[Environment]::NewLine,$Text
            Write-Error -Message $errorMsg -Exception $ex -ErrorAction $ErrorActionPreference
        }
        finally
        {
            $lines = @()
            if ($verboseEnabled)
            {
                $lines = & {
                    if ($cmd.Parameters.Count)
                    {
                        $paramFieldLength =
                            $cmd.Parameters |
                            Select-Object -ExpandProperty 'ParameterName' |
                            Select-Object -ExpandProperty 'Length' |
                            Measure-Object -Maximum |
                            Select-Object -ExpandProperty 'Maximum'
                        for ($idx = 0 ; $idx -lt $cmd.Parameters.Count ; ++$idx)
                        {
                            $param = $cmd.Parameters[$idx]
                            $paramName = $param.ParameterName.PadRight($paramFieldLength)
                            $paramValue = $param.Value
                            "${paramName} ${paramValue}" | Write-Output
                        }
                    }

                    $Text -split '\r?\n' | Write-Output
                }
            }

            $cmd.Dispose()

            $stopwatch.Stop()

            # Only calculate and output timings if verbose output is enabled. We don't even call Write-Verbose because
            # some queries could have sensitive information in them, and in order to prevent them from being visible,
            # the user will add `-Verbose:$false` when calling this function. I'm being extra cautious here so there is
            # no way for someone to intercept sensitive queries.
            if ($verboseEnabled)
            {
                $duration = $stopwatch.Elapsed
                if( $duration.TotalHours -ge 1 )
                {
                    $durationDesc = '{0,2}h {1,3}m ' -f [int]$duration.TotalHours,$duration.Minutes
                }
                elseif( $duration.TotalMinutes -ge 1 )
                {
                    $durationDesc = '{0,2}m {1,3}s ' -f [int]$duration.TotalMinutes,$duration.Seconds
                }
                elseif( $duration.TotalSeconds -ge 1 )
                {
                    $durationDesc = '{0,2}s {1,3}ms' -f [int]$duration.TotalSeconds,$duration.Milliseconds
                }
                else
                {
                    $durationDesc = '{0,7}ms' -f [int]$duration.TotalMilliseconds
                }

                Write-Verbose -Message ('{0} {1}' -f $durationDesc, ($lines | Select-Object -First 1))
                foreach( $line in ($lines | Select-Object -Skip 1))
                {
                    Write-Verbose -Message ('{0} {1}' -f (' ' * $durationDesc.Length), $line)
                }
            }
        }
    }
}


# Copyright 2012 Aaron Jensen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every function/cmdlet call in your function. Please vote up this issue so it can get fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        $Cmdlet,

        [Parameter(Mandatory = $true)]
        [Management.Automation.SessionState]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        $SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }

}


function ConvertTo-YMsSqlIdentifier
{
    <#
    .SYNOPSIS
    Converts a string to a Microsoft SQL Server quoted identifier.
 
    .DESCRIPTION
    The `ConvertTo-YMsSqlIdentifier` function converts a string to a quoted identifier. Pipe the name to the function
    (or pass it to the `ImputObject` parameter). The function calls SQL Server's `QUOTENAME` function to quote and
    escape the name. The value is passed to the query as a parameter to avoid SQL injection attacks.
 
    Use this function if embedding object names in strings containing raw SQL.
 
    .EXAMPLE
    'hello[]world' | ConvertTo-YMsSqlIdentifier -Connection $conn
 
    Demonstrates how to pipe values to this function. In this example, the function calls
    `select quotename('hello[]world')`, which would return the string `[hello[]]world]`.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The value to convert.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $InputObject
    )

    process
    {
        return Invoke-YMsSqlCommand -Connection $Connection `
                                    -Text 'select QUOTENAME(@name)' `
                                    -Parameter @{ '@name' = $InputObject } `
                                    -AsScalar
    }
}


function Get-YMsSqlExtendedProperty
{
    <#
    .SYNOPSIS
    Gets Microsoft SQL Server extended properties.
 
    .DESCRIPTION
    The `Get-YMsSqlExtendedProperty` function gets extended property metadata for objects in a Microsoft SQL Server. It
    calls the `fn_listextendedproperty` to get the extended properties. To get all extended properties for arbitrary
    objects, pass appropriate values level 0, 1, and types and names to the `Level0Type`, `Level0Name`, `Level1Type`,
    `Level1Name`, `Level2Type`, and `Level2Name` parameters. To get a specific extended property, pass its name to the
    `Name` parameter.
 
    To get extended properties for a schema, pass the schema's name to the `SchemaName` parameter.
 
    To get extended properties for a table, pass the table's name and schema name to the `TableName` and `SchemaName`
    parameters, respectively.
 
    To get extended properties for a column on a table, pass the column's name, table name, and table's schema name to
    the `ColumnName`, `TableName`, and `SchemaName` parameters, respectively.
 
    To use `NULL` as the value for any of the `fn_listextendedproperty` level or name parameters, omit the argument or
    pass `$null`.
 
    To use `default` as the value for any of the `fn_listextendedproperty` level or name parameters, pass
    `[Yodel_MsSql_QueryKeyword]::Default`.
 
    .EXAMPLE
    Get-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel'
 
    Demonstrates how to get a schema's extended properties by passing the schema name to the `SchemaName` parameter.
 
    .EXAMPLE
    Get-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1'
 
    Demonstrates how to get a table's extended properties by passing the table name to the `TableName` parameter and the
    table's schema to the `SchemaName` parameter.
 
    .EXAMPLE
    Get-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1' -ColumnName 'id
 
    Demonstrates how to get a table column's extended properties by passing the column name to the `ColumnName`
    parameter and the columns's table to the `TableName` parameter and the table's schema to the `SchemaName` parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='Raw')]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema name whose extended properties to get. Or the schema name of the table whose extended properties to
        # get. Defaults to `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForSchema')]
        [Parameter(ParameterSetName='ForTable')]
        [Parameter(ParameterSetName='ForTableColumn')]
        [String] $SchemaName,

        # The table name whose extended properties to get, or the table name of the column whose extended properties to
        # get. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForTable')]
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $TableName,

        # The column name whose extended properties to get. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $ColumnName,

        # The name of the extended property to get. Defalt is `NULL`. To use `default` as the value, pass
        # `[Yodel_MsSql_QueryKeyword]::Default`.
        [Object] $Name,

        # The value for the `fn_listextendedproperty` function's `level0_object_type` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Type,

        # The value for the `fn_listextendedproperty` function's `level0_object_name` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Name,

        # The value for the `fn_listextendedproperty` function's `level1_object_type` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Type,

        # The value for the `fn_listextendedproperty` function's `level1_object_name` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Name,

        # The value for the `fn_listextendedproperty` function's `level2_object_type` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Type,

        # The value for the `fn_listextendedproperty` function's `level2_object_name` parameter. Default is `NULL`. To
        # use `default` as the value, pass `[Yodel_MsSql_QueryKeyword]::Default`.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -notlike 'Raw*')
    {
        if (-not $SchemaName)
        {
            $SchemaName = 'dbo'
        }

        $l1Type = $l1Name = $l2Type = $l2Name = $null

        if ($PSCmdlet.ParameterSetName -ne 'ForSchema')
        {
            $l1Type = 'table'
            $l1Name = $TableName

            $l2Type = $null
            $l2Name = $null
            if ($PSBoundParameters.ContainsKey('ColumnName'))
            {
                $l2Type = 'column'
                $l2Name = $ColumnName
            }
        }

        return Get-YMsSqlExtendedProperty -Connection $Connection `
                                         -Name $Name `
                                         -Level0Type 'schema' `
                                         -Level0Name $SchemaName `
                                         -Level1Type $l1Type `
                                         -Level1Name $l1Name `
                                         -Level2Type $l2Type `
                                         -Level2Name $l2Name
    }

    $parameter = @{}
    function Get-ArgValue
    {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, ValueFromPipeline)]
            [AllowNull()]
            [Object] $InputObject
        )

        if ($null -eq $InputObject)
        {
            return 'NULL'
        }

        if ($InputObject -eq [Yodel_MsSql_QueryKeyword]::Default)
        {
            return 'default'
        }

        $paramName = "@param$($parameter.Count)"
        $parameter[$paramName] = $InputObject
        return $paramName
    }

    $nameArg = $Name | Get-ArgValue
    $l0TypeArg = $Level0Type | Get-ArgValue
    $l0NameArg = $Level0Name | Get-ArgValue
    $l1TypeArg = $Level1Type | Get-ArgValue
    $l1NameArg = $Level1Name | Get-ArgValue
    $l2TypeArg = $Level2Type | Get-ArgValue
    $l2NameArg = $Level2Name | Get-ArgValue

    $query = "select * from sys.fn_listextendedproperty(${nameArg}, ${l0TypeArg}, ${l0NameArg}, ${l1TypeArg}, ${l1NameArg}, ${l2TypeArg}, ${l2NameArg})"
    $result = Invoke-YMsSqlCommand -Connection $Connection -Text $query -Parameter $parameter

    if ($result)
    {
        return $result
    }

    $msg = 'There are no extended properties on '

    if ($PSCmdlet.ParameterSetName -eq 'Raw')
    {
        $msg = "${msg}database ""$($Connection.Database)"""
    }

    function Get-LevelMessage
    {
        param(
            [Parameter(Mandatory)]
            [AllowNull()]
            [Object] $Type,

            [Parameter(Mandatory)]
            [AllowNull()]
            [Object] $Name
        )

        if ($null -eq $Type -or $Type -eq [Yodel_MsSql_QueryKeyword]::Default)
        {
            return ''
        }

        if ($null -eq $Name -or $Name -eq [Yodel_MsSql_QueryKeyword]::Default)
        {
            return "all ${Type}"
        }

        return "${Type} ""${Name}"""
    }

    $seperator = ''
    if ($PSCmdlet.ParameterSetName -in @('RawL0', 'RawL1', 'RawL2'))
    {
        $levelMsg = Get-LevelMessage -Type $Level0Type -Name $Level0Name
        $msg = "${msg}${levelMsg}"
        if ($levelMsg)
        {
            $seperator = ', '
        }
    }

    if ($PSCmdlet.ParameterSetName -in @('RawL1', 'RawL2'))
    {
        $levelMsg = Get-LevelMessage -Type $Level1Type -Name $Level1Name
        if ($levelMsg)
        {
            $msg = "${msg}${seperator}${levelMsg}"
            $seperator = ', '
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'RawL2')
    {
        $levelMsg = Get-LevelMessage -Type $Level2Type -Name $Level2Name
        if ($levelMsg)
        {
            $msg = "${msg}${seperator}${levelMsg}"
        }
    }

    Write-Error -Message "${msg}." -ErrorAction $ErrorActionPreference
}



function Get-YMsSqlSchema
{
    <#
    .SYNOPSIS
    Gets schema metadata from Microsoft SQL Server.
 
    .DESCRIPTION
    The `Get-YMsSqlSchema` function gets schema metadata from Microsoft SQL Server. Pass the connection to SQL Server to
    the `Connection` parameter and the name of the schema to the `Name` parameter. The function returns all columns
    from `sys.schemas` for the given schema.
 
    .EXAMPLE
    Get-YMsSqlSchema -Connection $conn -Name 'dbo'
 
    Demonstrates how to use this function. In this example, all columns from the `sys.schemas` table for the `dbo`
    schema will be returned.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $query = 'select * from sys.schemas where name = @schemaName'
    $result = Invoke-YMsSqlCommand -Connection $Connection -Text $query -Parameter @{ '@schemaName' = $Name }
    if (-not $result)
    {
        $msg = "Schema ""${Name}"" does not exist."
        Write-Error $msg -ErrorAction $ErrorActionPreference
        return
    }

    return $result
}


function Get-YMsSqlTable
{
    <#
    .SYNOPSIS
    Gets metadata about a table from Microsoft SQL Server.
 
    .DESCRIPTION
    the `Get-YMsSqlTable` function gets metadata about a table from Microsoft SQL Server. Pass the connection to SQL
    Server to the `Connection` parameter and the table name to the `Name` parameter. The function returns the result of
    select all the columns for that table from `sys.tables`.
 
    If the table isn't in the `dbo` schema, pass its schema name to the `SchemaName` parameter.
 
    .EXAMPLE
    Get-YMsSqlTable -Connection $conn -Name 'Table_1'
 
    Demonstrates how to get metadata for a table in the `dbo` schema. In this example, returns the `[dbo].[Table_1]`
    table's record from `sys.tables`.
 
    .EXAMPLE
    Get-YMsSqlTable -Connection $conn -SchemaName 'yodel' -Name 'Table_1'
 
    Demonstrates how to get metadata for a table in a custom schema. In this example, returns the `[yodel].[Table_1]`
    table's record from `sys.tables`.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The table's schema. Defaults to `dbo`.
        [String] $SchemaName = 'dbo',

        # The table name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $query = @"
select
    t.*
from
    sys.tables t
        join
    sys.schemas s
            on t.schema_id=s.schema_id
where
    t.name=@name and s.name=@schemaName
"@

    $queryArgs = @{
        '@name' = $Name;
        '@schemaName' = $SchemaName;
    }
    $result = Invoke-YMsSqlCommand -Connection $Connection -Text $query -Parameter $queryArgs
    if (-not $result)
    {
        $msg = "Table [${SchemaName}].[${Name}] does not exist."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    return $result
}


function Initialize-YMsSqlDatabase
{
    <#
    .SYNOPSIS
    Esnures a Microsoft SQL Server database exists.
 
    .DESCRIPTION
    The `Initialize-YMsSqlDatabase` function creates a database in Microsoft SQL Server if it doesn't already exist.
    Pass the connection to SQL Server to the `Connection` parameter and the name of the database to the `Name`
    parameter. If the database doesn't exist (i.e. there's no record for it in `sys.databases`), it is created.
 
    .EXAMPLE
    Initialize-YMsSqlDatabase -Connection $conn -Name 'Yodel'
 
    Demonstrates how to use this function to ensure a database exists. In this example, `Initialize-YMsSqlDatabase` will
    create the `Yodel` database if and only if the `Yodel` database doesn't exist.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The database name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $query = 'select 1 from sys.databases where name=@name'
    $dbExists = Invoke-YMsSqlCommand -Connection $Connection -Text $query -Parameter @{ '@name' = $Name } -AsScalar
    if ($dbExists)
    {
        return
    }

    $Name = $Name | ConvertTo-YMsSqlIdentifier -Connection $Connection
    $query = "create database ${Name}"
    Invoke-YMsSqlCommand -Connection $Connection -Text $query -NonQuery
}


function Initialize-YMsSqlSchema
{
    <#
    .SYNOPSIS
    Ensures a schema exists in a Microsoft SQL Server database.
 
    .DESCRIPTION
    The `Initialize-YMsSqlSchema` function creates a schema in a SQL Server database if that schema doesn't already
    exist. Pass the connection to the SQL server to use to the `Connection` parameter and the name of the schema to
    the `Name` parameter.
 
    .EXAMPLE
    Initialize-YMsSqlSchema -Connection $conn -Name 'Yodel'
 
    Demonstrates how to use this function. In this example, the `Yodel` schema will be created, but only if it doesn't
    already exist.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema's name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if (Test-YMsSqlSchema -Connection $Connection -Name $Name)
    {
        return
    }

    $Name = $Name | ConvertTo-YMsSqlIdentifier -Connection $Connection
    $query = "create schema ${Name}"
    Invoke-YMsSqlCommand -Connection $Connection -Text $query -NonQuery
}


function Invoke-YMsSqlCommand
{
    <#
    .SYNOPSIS
    Uses ADO.NET to execute a query in a SQL Server database.
 
    .DESCRIPTION
    `Invoke-YMsSqlCommand` executes a SQL query against a SQL Server database. The function opens a connection to SQL
    Server, executes the query, then closes the connection. Pass the name of the SQL Server (hostname and instance name)
    to the `SqlServerName` parameter. Pass the database name to the `DatabaseName` parameter. By default, the query is
    run as the current user. To run as a custom user, pass the user's credentials to the `Credential` parameter.
 
    Pass the query to run to the `Text` parameter. You may also pipe queries to `Invoke-YMsSqlCommand`. Piped queries
    are all run using the same connection.
 
    The function returns a generic object for each row in the result set. Each object has a property for each column. If
    a column doesn't have a name, a generic `ColumnX` name is assigned, where `X` starts at 0 and increases by one for
    each nameless column.
 
    If your query returns a single value, use the `-AsScalar` switch.
 
    If your query returns no results, use the `-NonQuery` switch. If your query affects any rows, the number of rows
    affected will be returned.
 
    To run a parameterized query, use `@name` parameters in your query and pass the values of those parameters in a
    hashtable to the `Parameter` parameter, e.g. `@{ '@name' = 'the_name' }`.
 
    To execute a stored procedure, set the `Text` parameter to the name of the stored procedure, set the `Type`
    parameter to `[Data.CommandType]::StoredProcedure`, and pass the procedure's parameters to the `Parameter` parameter
    (a hashtable of parameter names and values).
 
    Queries will time out after 30 seconds (the default .NET timeout). If you have a query that runs longer, pass the
    number of seconds to wait to the `Timeout` parameter.
 
    Query timings are output to the verbose stream, including the text of the query. If you want to suppress sensitive
    queries from being output, set the `Verbose` parameter to `$false`, e.g. `-Verbose:$false`.
 
    The `Invoke-YMsSqlCommand` function constructs a connection string for you based on the values of the
    `SqlServerName` and `DatabaseName` parameters. If you have custom properties you'd like added to the connection
    string, pass them to the `ConnectionString` parameter.
 
    Failed queries do not cause a terminating error. If you want your script to stop if your query fails, set the
    `ErrorAction` parameter to `Stop`, e.g. `-ErrorAction Stop`.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'select * from MyTable'
 
    Demonstrates how to select rows from a table.
 
    .EXAMPLE
    'select 1','select 2' | Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master'
 
    Demonstrates that you can pipe commands to `Invoke-YMsSqlCommand`. All queries piped to `Invoke-YMsSqlCommand` are
    run using the same connection.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'select count(*) from MyTable' -AsScalar
 
    Demonstrates how to return a scalar value. If the command returns multiple rows/columns, returns the first row's
    first column's value.
 
    .EXAMPLE
    $rowsDeleted = Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'delete from dbo.Example' -NonQuery
 
    Demonstrates how to execute a command that doesn't return a value. If your command updates/deletes rows, the number
    of rows affected is returned.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'insert into MyTable (Two,Three) values @Column2, @Column3' -Parameter @{ '@Column2' = 'Value2'; '@Column3' = 'Value3' } -NonQuery
 
    Demonstrates how to use parameterized queries.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'sp_addrolemember' -CommandType [Data.CommandType]::StoredProcedure -Parameter @{ '@rolename' = 'db_owner'; '@membername' = 'myuser'; }
 
    Demonstrates how to execute a stored procedure, including how to pass its parameters using the `Parameter`
    parameter.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'create login [yodeltest] with password ''P@$$w0rd''' -Verbose:$false
 
    Demonstrates how to prevent command timings for sensitive queries from being written to the verbose stream.
 
    .EXAMPLE
    Invoke-YMsSqlCommand -SqlServerName '.' -DatabaseName 'master' -Text 'select * from a_really_involved_join_that_takes_a_long_time' -Timeout 120
 
    Demonstrates how to set the command timeout for commands that take longer than .NET's default timeout (30 seconds).
    #>

    [CmdletBinding(DefaultParameterSetName='AdHoc_ExecuteReader')]
    param(
        # The database connection to use.
        [Parameter(Mandatory, ParameterSetName='Connection_ExecuteNonQuery')]
        [Parameter(Mandatory, ParameterSetName='Connection_ExecuteReader')]
        [Parameter(Mandatory, ParameterSetName='Connection_ExecuteScalar')]
        [Data.Common.DbConnection] $Connection,

        # The SQL Server instance to connect to.
        [Parameter(Mandatory, Position=0, ParameterSetName='AdHoc_ExecuteNonQuery')]
        [Parameter(Mandatory, Position=0, ParameterSetName='AdHoc_ExecuteReader')]
        [Parameter(Mandatory, Position=0, ParameterSetName='AdHoc_ExecuteScalar')]
        [String] $SqlServerName,

        # The database to connect to.
        [Parameter(Mandatory, Position=1, ParameterSetName='AdHoc_ExecuteNonQuery')]
        [Parameter(Mandatory, Position=1, ParameterSetName='AdHoc_ExecuteReader')]
        [Parameter(Mandatory, Position=1, ParameterSetName='AdHoc_ExecuteScalar')]
        [String] $DatabaseName,

        [Parameter(ParameterSetName='AdHoc_ExecuteNonQuery')]
        [Parameter(ParameterSetName='AdHoc_ExecuteReader')]
        [Parameter(ParameterSetName='AdHoc_ExecuteScalar')]
        [ValidateNotNullOrEmpty()]
        [pscredential] $Credential,

        # The connection string to use.
        [Parameter(ParameterSetName='AdHoc_ExecuteNonQuery')]
        [Parameter(ParameterSetName='AdHoc_ExecuteReader')]
        [Parameter(ParameterSetName='AdHoc_ExecuteScalar')]
        [String] $ConnectionString,

        # The command to run/execute.
        [Parameter(Mandatory, Position=2, ValueFromPipeline)]
        [String] $Text,

        # Any parameters used in the command.
        [hashtable] $Parameter,

        # Return the result as a single value instead of a row. If the command returns multiple rows/columns, the value
        # of the first row's first column is returned.
        [Parameter(Mandatory, ParameterSetName='AdHoc_ExecuteScalar')]
        [Parameter(Mandatory, ParameterSetName='Connection_ExecuteScalar')]
        [switch] $AsScalar,

        # Executes a command that doesn't return any records. For updates/deletes, the number of rows affected will be
        # returned unless the NOCOUNT option is used.
        [Parameter(Mandatory, ParameterSetName='AdHoc_ExecuteNonQuery')]
        [Parameter(Mandatory, ParameterSetName='Connection_ExecuteNonQuery')]
        [switch] $NonQuery,

        # The time (in seconds) to wait for a command to execute. The default is .NET's default timeout, which is 30
        # seconds.
        [int] $Timeout,

        # The type of command being run. The default is Text, or a plain query.
        [Data.CommandType] $Type = [Data.CommandType]::Text
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

        $isMyConn = $false
        if (-not $Connection)
        {
            $optionalParams = @{}
            if( $Credential )
            {
                $optionalParams['Credential'] = $Credential
            }

            if( $ConnectionString )
            {
                $optionalParams['ConnectionString'] = $ConnectionString
            }

            $connection = Connect-YDatabase -SqlServerName $SqlServerName -DatabaseName $DatabaseName @optionalParams
            $isMyConn = $true
        }

        $optionalParams = @{}

        if( $AsScalar )
        {
            $optionalParams['AsScalar'] = $true
        }

        if( $NonQuery )
        {
            $optionalParams['NonQuery'] = $true
        }

        if( $Timeout )
        {
            $optionalParams['Timeout'] = $Timeout
        }

        if( $Type )
        {
            $optionalParams['Type'] = $Type
        }

        if( $Parameter )
        {
            $optionalParams['Parameter'] = $Parameter
        }
    }

    process
    {
        $cmdFailed = $true
        try
        {
            Invoke-YDbCommand -Connection $Connection -Text $Text @optionalParams
            $cmdFailed = $false
        }
        finally
        {
            # Terminating errors stop the pipeline.
            if( $cmdFailed )
            {
                if ($isMyConn)
                {
                    $Connection.Close()
                }
            }
        }
    }

    end
    {
        if ($isMyConn)
        {
            $connection.Close()
        }
    }
}

function Invoke-YSqlServerCommand
{
    <#
    .SYNOPSIS
    ***OBSOLETE.*** Use `Invoke-YMsSqlCommand` instead.
 
    .DESCRIPTION
    ***OBSOLETE.*** Use `Invoke-YMsSqlCommand` instead.
 
    .EXAMPLE
    ***OBSOLETE.*** Use `Invoke-YMsSqlCommand` instead.
    #>

    [CmdletBinding(DefaultParameterSetName='ExecuteReader')]
    param(
        [Data.Common.DbConnection] $Connection,

        [Parameter(Mandatory, Position=0)]
        [String] $SqlServerName,

        [Parameter(Mandatory, Position=1)]
        [String] $DatabaseName,

        [ValidateNotNullOrEmpty()]
        [pscredential] $Credential,

        [String] $ConnectionString,

        [Parameter(Mandatory, Position=2, ValueFromPipeline)]
        [String] $Text,

        [hashtable] $Parameter,

        [Parameter(Mandatory, ParameterSetName='ExecuteScalar')]
        [switch] $AsScalar,

        [Parameter(Mandatory, ParameterSetName='ExecuteNonQuery')]
        [switch] $NonQuery,

        [int] $Timeout,

        [Data.CommandType] $Type = [Data.CommandType]::Text
    )

    process
    {
        $msg = 'Invoke-YMsSqlCommand is obsolete and will be removed in the next major version of Yodel. Please use ' +
            'Invoke-YMsSqlCommand instead.'
        Write-Warning -Message $msg

        Invoke-YMsSqlCommand @PSBoundParameters
    }
}



function Remove-YMsSqlTable
{
    <#
    .SYNOPSIS
    Drops a table from Microsoft SQL Server.
 
    .DESCRIPTION
    The `Remove-YMsSqlTable` function drops a table in SQL Server. Pass the connection to SQL Server to the `Connection`
    parameter and the table name to the `Name` parameter. If the table doesn't exist, the function writes an error.
 
    If the table isn't in the `dbo` schema, pass its schema to the `SchemaName` parameter.
 
    .EXAMPLE
    Remove-YMsSqlTable -Connection $conn -Name 'Table_2'
 
    Demonstrates how to remove the `[dbo].[Table_2]` table.
 
    .EXAMPLE
    Remove-YMsSqlTable -Connection $conn -SchemaName 'yodel' -Name 'Table_2'
 
    Demonstrates how to remove the `[yodel].[Table_2]` table.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The table's schema name. Defaults to `dbo`.
        [String] $SchemaName = 'dbo',

        # The table's name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if (-not (Test-YMsSqlTable -Connection $Connection -SchemaName $SchemaName -Name $Name))
    {
        $msg = "Table ${SchemaName}.${Name} does not exist."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    $SchemaName, $Name = $SchemaName, $Name | ConvertTo-YMsSqlIdentifier -Connection $Connection
    $query = "drop table ${SchemaName}.${Name}"
    Invoke-YMsSqlCommand -Connection $Connection -Text $query -NonQuery
}


function Set-YMsSqlExtendedProperty
{
    <#
    .SYNOPSIS
    Adds or updates Microsoft SQL Server extended properties.
 
    .DESCRIPTION
    The `Set-YMsSqlExtendedProperty` function adds or updates extended property metadata for objects in SQL Server. It
    calls the `fn_addextendedproperty` to add a new extended property and `fn_updateextendedproperty` to update an
    existing extended property. An extended property is updated only if its value has changed. Pass the connection to
    Microsoft SQL Server to the connection parameter, the name of the extended property to the `Name` parameter and the
    value to the `Value` property.
 
    To set an extended property for an arbitrary object, pass the appropriate level 0, 1, and 2 types and names for that
    object to the `Level0Type`, `Level0Name`, `Level1Type`, `Level1Name`, `Level2Type`, and `Level2Name` parameters. The
    value `$null` is allowed for any level type/name parameter.
 
    To set an extended property for a schema, pass the schema's name to the `SchemaName` parameter.
 
    To set an extended property for a table, pass the table's name and schema name to the `TableName` and `SchemaName`
    parameters, respectively.
 
    To set an extended property for a column on a table, pass the column's name, table name, and table's schema name to
    the `ColumnName`, `TableName`, and `SchemaName` parameters, respectively.
 
    .EXAMPLE
    Set-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -Name 'Yodel_Example' -Value '1'
 
    Demonstrates how to set a schema extended property by passing the schema name to the `SchemaName` parameter.
 
    .EXAMPLE
    Set-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1' -Name 'Yodel_Example' -Value '2'
 
    Demonstrates how to set a table extended properties by passing the table name to the `TableName` parameter and the
    table's schema to the `SchemaName` parameter.
 
    .EXAMPLE
    Set-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1' -ColumnName 'id' -Name 'Yodel_Example' -Value '3'
 
    Demonstrates how to set a table column extended property by passing the column name to the `ColumnName` parameter,
    the columns's table name to the `TableName` parameter and the table's schema name to the `SchemaName` parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='Raw')]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema name whose extended property to set. Or the schema name of the table whose extended propert to
        # set. Defaults to `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForSchema')]
        [Parameter(ParameterSetName='ForTable')]
        [Parameter(ParameterSetName='ForTableColumn')]
        [String] $SchemaName,

        # The table name whose extended property to set, or the table name of the column whose extended property to
        # set. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForTable')]
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $TableName,

        # The column name whose extended property to set. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $ColumnName,

        # The name of the extended property to set. Defalt is `NULL`.
        [Parameter(Mandatory)]
        [String] $Name,

        # The name of the extended property to set. Defalt is `NULL`.
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [AllowNull()]
        [String] $Value,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level0_object_type`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Type,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level0_object_name`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Name,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level1_object_type`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Type,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level1_object_name`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Name,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level2_object_type`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Type,

        # The value for the `fn_addextendedproperty` or `fn_updateextededproperty` function's `level2_object_name`
        # parameter. Default is `NULL`.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -notlike 'Raw*')
    {
        if (-not $SchemaName)
        {
            $SchemaName = 'dbo'
        }

        $setArgs = @{
            Connection = $Connection;
            Name = $Name;
            Value = $Value;
            Level0Type = 'schema';
            Level0Name = $SchemaName;
        }

        if ($PSCmdlet.ParameterSetName -ne 'ForSchema')
        {
            $setArgs['Level1Type'] = 'table'
            $setArgs['Level1Name'] = $TableName

            if ($PSBoundParameters.ContainsKey('ColumnName'))
            {
                $setArgs['Level2Type'] = 'column'
                $setArgs['Level2Name'] = $ColumnName
            }
        }

        return Set-YMsSqlExtendedProperty @setArgs
    }

    $propArgs = @{}
    foreach ($argPosition in '0', '1', '2')
    {
        foreach ($argKind in @('Type', 'Name'))
        {
            $argName = "Level${argPosition}${argKind}"
            if (-not $PSBoundParameters.ContainsKey($argName))
            {
                continue
            }
            $propArgs[$argName] = $PSBoundParameters[$argName]
        }
    }
    $prop = Get-YMsSqlExtendedProperty -Connection $Connection -Name $Name @propArgs -ErrorAction Ignore
    if ($prop -and $prop.value -eq $Value)
    {
        return
    }

    $sprocArgs = @{
        '@name' = $Name;
        '@value' = $Value;
    }
    foreach ($argPosition in '0', '1', '2')
    {
        foreach ($argKind in @('type', 'name'))
        {
            $argName = "level${argPosition}${argKind}"
            if (-not $PSBoundParameters.ContainsKey($argName))
            {
                continue
            }
            $sprocArgs["@${argName}"] = $PSBoundParameters[$argName]
        }
    }

    $sprocName = 'sp_updateextendedproperty'
    if ($null -eq $prop)
    {
        $sprocName = 'sp_addextendedproperty'
    }

    Invoke-YMsSqlCommand -Connection $Connection `
                         -Text $sprocName `
                         -Parameter $sprocArgs `
                         -Type ([Data.CommandType]::StoredProcedure)
}



function Test-YMsSqlExtendedProperty
{
    <#
    .SYNOPSIS
    Tests for the existence of Microsoft SQL Server extended properties.
 
    .DESCRIPTION
    The `Test-YMsSqlExtendedProperty` function tests if an extended property for an object exists in SQL Server. It
    calls the `fn_listextendedproperty` to get the extended property. Pass the connection to Microsoft SQL Server to the
    connection parameter and the name of the extended property to the `Name` parameter.
 
    To test for the existence of an extended property for an arbitrary object, pass the appropriate level 0, 1, and 2
    types and names for that object to the `Level0Type`, `Level0Name`, `Level1Type`, `Level1Name`, `Level2Type`, and
    `Level2Name` parameters. The value `$null` is allowed for any level type/name parameter. Use
    `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value for any `fn_listextendedproperty` parameter.
 
    To test for the existence of an extended property for a schema, pass the schema's name to the `SchemaName`
    parameter.
 
    To test for the existence of an extended property for a table, pass the table's name and schema name to the
    `TableName` and `SchemaName` parameters, respectively.
 
    To test for the existence of an extended property for a column on a table, pass the column's name, table name, and
    table's schema name to the `ColumnName`, `TableName`, and `SchemaName` parameters, respectively.
 
    .EXAMPLE
    Test-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -Name 'Yodel_Example'
 
    Demonstrates how to test if a schema extended property exists by passing the schema name to the `SchemaName`
    parameter.
 
    .EXAMPLE
    Test-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1' -Name 'Yodel_Example'
 
    Demonstrates how to test if a table extended properties exists by passing the table name to the `TableName`
    parameter and the table's schema to the `SchemaName` parameter.
 
    .EXAMPLE
    Test-YMsSqlExtendedProperty -Connection $conn -SchemaName 'yodel' -TableName 'Table_1' -ColumnName 'id' -Name 'Yodel_Example'
 
    Demonstrates how to test if a table column extended property exists by passing the column name to the `ColumnName`
    parameter, the columns's table name to the `TableName` parameter and the table's schema name to the `SchemaName`
    parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='Raw')]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema name whose extended property to test, or the schema name of the table whose extended property to
        # test.
        [Parameter(Mandatory, ParameterSetName='ForSchema')]
        [Parameter(ParameterSetName='ForTable')]
        [Parameter(ParameterSetName='ForTableColumn')]
        [String] $SchemaName,

        # The table name whose extended property to set, or the table name of the column whose extended property to
        # set.
        [Parameter(Mandatory, ParameterSetName='ForTable')]
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $TableName,

        # The column name whose extended property to set.
        [Parameter(Mandatory, ParameterSetName='ForTableColumn')]
        [String] $ColumnName,

        # The name of the extended property to test.
        [Parameter(Mandatory)]
        [String] $Name,

        # The value for the `fn_listextendedproperty` function's `level0_object_type` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Type,

        # The value for the `fn_listextendedproperty` function's `level0_object_name` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL0')]
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level0Name,

        # The value for the `fn_listextendedproperty` function's `level1_object_type` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Type,

        # The value for the `fn_listextendedproperty` function's `level1_object_name` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL1')]
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level1Name,

        # The value for the `fn_listextendedproperty` function's `level2_object_type` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Type,

        # The value for the `fn_listextendedproperty` function's `level2_object_name` parameter. Default is `NULL`. Use
        # `[Yodel_MsSql_QueryKeyword]::Default` to pass `default` as the value.
        [Parameter(Mandatory, ParameterSetName='RawL2')]
        [AllowNull()]
        [Object] $Level2Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $PSBoundParameters['ErrorAction'] = [Management.Automation.ActionPreference]::Ignore
    if (Get-YMsSqlExtendedProperty @PSBoundParameters)
    {
        return $true
    }

    return $false
}



function Test-YMsSqlSchema
{
    <#
    .SYNOPSIS
    Checks if a schema exists in Microsoft SQL Server.
 
    .DESCRIPTION
    The `Test-YMsSqlSchema` function tests if a schema exists in SQL Server. Pass the connection to SQL Server to the
    `Connection` parameter and the schema name to the `Name` parameter. Returns `$true` if the schema exists, and
    `$false otherwise.
 
    .EXAMPLE
    Test-YMsSqlSchema -Connection $conn -Name 'yodel'
 
    Demonstrates how to test if a schema exists by passing the schema name to the `Name` parameter.
    #>

    [CmdletBinding()]
    param(
        # The connection to Microsoft SQL Server.
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        # The schema name.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $schema = Get-YMsSqlSchema -Connection $Connection -Name $Name -ErrorAction Ignore
    if (-not $schema)
    {
        return $false
    }
    return $true
}


function Test-YMsSqlTable
{
    <#
    .SYNOPSIS
    Checks if a table exists in Microsoft SQL Server.
 
    .DESCRIPTION
    The `Test-YMsSqlTable` function tests if a table exists in SQL Server. Pass the connection to SQL Server to the
    `Connection` parameter and the table name to the `Name` parameter. Returns `$true` if the table exists, and `$false
    otherwise.
 
    .EXAMPLE
    Test-YMsSqlTable -Connection $conn -Name 'yodel'
 
    Demonstrates how to test if a table exists by passing the table name to the `Name` parameter.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Data.Common.DbConnection] $Connection,

        [String] $SchemaName = 'dbo',

        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    $table = Get-YMsSqlTable -Connection $Connection -SchemaName $SchemaName -Name $Name -ErrorAction Ignore
    if (-not $table)
    {
        return $false
    }
    return $true
}