Send-SQLDataToExcel.ps1

Function Send-SQLDataToExcel {
        <#
      .SYNOPSIS
        Inserts a DataTable - returned by a SQL query - into an ExcelSheet
      .DESCRIPTION
        This command takes a SQL statement and run it against a database connection; for the connection it accepts either
        * an object representing a session with a SQL server or ODBC database, or
        * a connection string to make a session (if -MSSQLServer is specified it uses the SQL Native client,
          and -Connection can be a server name instead of a detailed connection string. Without this switch it uses ODBC)
        The command takes all the parameters of Export-Excel, except for -InputObject (alias TargetData); after
        fetching the data it calls Export-Excel with the data as the value of InputParameter and whichever of
        Export-Excel's parameters it was passed; for details of these parameters see the help for Export-Excel.
      .PARAMETER Session
        An active ODBC Connection or SQL connection object representing a session with a database which will be queried to get the data .
      .PARAMETER Connection
        A database connection string to be used to create a database session; either
        * A Data source name written in the form DSN=ODBC_Data_Source_Name, or
        * A full ODBC or SQL Native Client Connection string, or
        * The name of a SQL server.
      .PARAMETER MSSQLServer
        Specifies the connection string is for SQL server, not ODBC.
      .PARAMETER SQL
        The SQL query to run against the session which was passed in -Session or set up from -Connection.
      .PARAMETER Database
        Switches to a specific database on a SQL server.
      .PARAMETER QueryTimeout
        Override the default query time of 30 seconds.
      .PARAMETER DataTable
        A System.Data.DataTable object containing the data to be inserted into the spreadsheet without running a query.
        This remains supported to avoid breaking older scripts, but if you have a DataTable object you can pass the it
        into Export-Excel using -InputObject.
      .PARAMETER Force
        If specified Export-Excel will be called with parameters specified, even if there is no data to send
      .EXAMPLE
        C:\> Send-SQLDataToExcel -MsSQLserver -Connection localhost -SQL "select name,type,type_desc from [master].[sys].[all_objects]" -Path .\temp.xlsx -WorkSheetname master -AutoSize -FreezeTopRow -AutoFilter -BoldTopRow
 
        Connects to the local SQL server and selects 3 columns from [Sys].[all_objects] and exports then to a sheet named master with some basic header management
      .EXAMPLE
        C:\> $dbPath = 'C:\Users\James\Documents\Database1.accdb'
        C:\> $Connection = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=$dbPath;"
        C:\> $SQL="SELECT top 25 Name,Length From TestData ORDER BY Length DESC"
 
        C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo1.xlsx -WorkSheetname "Sizes" -AutoSize
 
        This creates an ODBC connection string to read from an Access file and a SQL Statement to extracts data from it,
        and sends the resulting data to a new worksheet
 
      .EXAMPLE
        C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx'
        C:\> $Connection = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};Dbq=$dbPath;"
        C:\> $SQL="SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " +
                   " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC"
 
        C:\> Send-SQLDataToExcel -Connection $connection -SQL $sql -path .\demo2.xlsx -WorkSheetname "Winners" -AutoSize -AutoNameRange -ConditionalFormat @{DataBarColor="Blue"; Range="Wins"}
 
        Similar to the previous example this creates a connection string, this time for an Excel file, and runs
        a SQL statement to get a list of motor-racing results, outputting the resulting data to a new spreadsheet.
        The spreadsheet is formatted and a data bar added to show make the drivers' wins clearer.
        (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg )
      .EXAMPLE
        C:\> $dbPath = 'C:\users\James\Documents\f1Results.xlsx'
        C:\> $SQL = "SELECT top 25 DriverName, Count(RaceDate) as Races, Count(Win) as Wins, Count(Pole) as Poles, Count(FastestLap) as Fastlaps " +
                     " FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC"
        C:\> $null = Get-SQL -Session F1 -excel -Connection $dbPath -sql $sql -OutputVariable Table
 
        C:\> Send-SQLDataToExcel -DataTable $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show
 
        This uses Get-SQL (at least V1.1 - download from the PowerShell gallery with Install-Module -Name GetSQL -
        note the function is Get-SQL the module is GetSQL without the "-" )
        Get-SQL simplify making database connections and building /submitting SQL statements.
        Here Get-SQL uses the same SQL statement as before; -OutputVariable leaves a System.Data.DataTable object in $table
        and Send-SQLDataToExcel puts $table into the worksheet and sets it as an Excel table.
        The command is equivalent to running
        C:\> Export-Excel -inputObject $Table -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -autosize -TableName winners -TableStyle Light6 -show
        This is quicker than using
        C:\> Get-SQL <parameters> | export-excel -ExcludeProperty rowerror,rowstate,table,itemarray,haserrors <parameters>
        (the F1 results database is available from https://1drv.ms/x/s!AhfYu7-CJv4ehNdZWxJE9LMAX_N5sg )
      .EXAMPLE
        C:\> $SQL = "SELECT top 25 DriverName, Count(Win) as Wins FROM Results GROUP BY DriverName ORDER BY (count(win)) DESC"
        C:\> Send-SQLDataToExcel -Session $DbSessions["f1"] -SQL $sql -Path ".\demo3.xlsx" -WorkSheetname Gpwinners -ClearSheet -autosize -ColumnChart
 
        Like the previous example, this uses Get-SQL (download from the gallery with Install-Module -Name GetSQL).
        It uses the database session which Get-SQL created, rather than an ODBC connection string.
        The Session parameter can either be a object (as shown here), or the name used by Get-SQL ("F1" in this case).
        Here the data is presented as a quick chart.
      .EXAMPLE
        C:\> Send-SQLDataToExcel -path .\demo4.xlsx -WorkSheetname "LR" -Connection "DSN=LR" -sql "SELECT name AS CollectionName FROM AgLibraryCollection Collection ORDER BY CollectionName"
 
        This example uses an Existing ODBC datasource name "LR" which maps to an adobe lightroom database and gets a list of collection names into a worksheet
 
    .Link
        Export-Excel
    #>

    [CmdletBinding(DefaultParameterSetName="none")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification="Allowed to use DBSessions Global variable from GETSQL Module")]

    param (
        [Parameter(ParameterSetName="SQLConnection",   Mandatory=$true)]
        [Parameter(ParameterSetName="ODBCConnection",  Mandatory=$true)]
        $Connection,
        [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)]
        $Session,
        [Parameter(ParameterSetName="SQLConnection",   Mandatory=$true)]
        [switch]$MsSQLserver,
        [Parameter(ParameterSetName="SQLConnection")]
        [String]$DataBase,
        [Parameter(ParameterSetName="SQLConnection",   Mandatory=$true)]
        [Parameter(ParameterSetName="ODBCConnection",  Mandatory=$true)]
        [Parameter(ParameterSetName="ExistingSession", Mandatory=$true)]
        [string]$SQL,
        [int]$QueryTimeout,
        [Parameter(ParameterSetName="Pre-FetchedData", Mandatory=$true)]
        [System.Data.DataTable]$DataTable,
        [switch]$Force
    )
#Import the parameters from Export-Excel, we will pass InputObject, and we have the common parameters so exclude those,
#and re-write the [Parmameter] attribute on each one to avoid parameterSetName here competing with the settings in Export excel.
#The down side of this that impossible parameter combinations won't be filtered out and need to be caught later.
    DynamicParam {
        $ParameterAttribute  =                        "System.Management.Automation.ParameterAttribute"
        $RuntimeDefinedParam =                        "System.Management.Automation.RuntimeDefinedParameter"
        $paramDictionary     =    New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
        $attributeCollection =    New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
        $attributeCollection.Add((New-Object -TypeName $ParameterAttribute -Property @{ ParameterSetName = "__AllParameterSets" ;Mandatory = $false}))
        foreach ($P in (Get-Command -Name Export-Excel).Parameters.values.where({$_.name -notmatch 'Verbose|Debug|Action$|Variable$|Buffer$|TargetData$|InputObject$'}))  {
            $paramDictionary.Add($p.Name, (New-Object -TypeName $RuntimeDefinedParam -ArgumentList $p.name, $p.ParameterType, $attributeCollection ) )
        }
        return $paramDictionary
    }
    process {
      #region Dynamic params mean we can get passed parameter combination Export-Excel will reject, so throw here, rather than get data and then have Export-Excel error.
        if ($PSBoundParameters.Path -and $PSBoundParameters.ExcelPackage) {
            throw 'Parameter error: you cannot specify both a path and an Excel Package.'
            return
        }
        if ($PSBoundParameters.AutoFilter -and ($PSBoundParameters.TableName -or $PSBoundParameters.TableStyle)) {
            Write-Warning "Tables are automatically auto-filtered, -AutoFilter will be ignored"
            $null = $PSBoundParameters.Remove('AutoFilter')
        }
      #endregion
      #region if we were either given a session object or a connection string (& optionally -MSSQLServer) make sure we can connect
        try {
            #If we got -MSSQLServer, create a SQL connection, if we didn't but we got -Connection create an ODBC connection
            if     ($MsSQLserver -and $Connection) {
                if ($Connection -notmatch '=') {$Connection = "server=$Connection;trusted_connection=true;timeout=60"}
                $Session     = New-Object -TypeName System.Data.SqlClient.SqlConnection  -ArgumentList $Connection
                if ($Session.State -ne 'Open') {$Session.Open()}
                if ($DataBase) {$Session.ChangeDatabase($DataBase) }
            }
            elseif ($Connection)  {
                $Session     = New-Object -TypeName System.Data.Odbc.OdbcConnection      -ArgumentList $Connection ; $Session.ConnectionTimeout = 30
            }
        }
        catch {
            Write-Warning "An Error occured trying to connect to $Connection, the error was $([Environment]::NewLine + $_.Exception.InnerException))"
        }
        if ($Session -is [String] -and $Global:DbSessions[$Session]) {$Session = $Global:DbSessions[$Session]}
      #endregion
      #region we may have been given a table, but if there is a db session to connect to, send the query
        if     ($Session) {
            try {
                #If the session a SQL one make a SQL DataAdapter, otherwise make an ODBC one
                if ($Session.GetType().name -match "SqlConnection") {
                    $dataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList (
                                New-Object -TypeName System.Data.SqlClient.SqlCommand     -ArgumentList $SQL, $Session)
                }
                else {
                    $dataAdapter = New-Object -TypeName System.Data.Odbc.OdbcDataAdapter     -ArgumentList (
                                New-Object -TypeName System.Data.Odbc.OdbcCommand         -ArgumentList $SQL, $Session )
                }
                if ($QueryTimeout) {$dataAdapter.SelectCommand.CommandTimeout = $QueryTimeout}

                #Both adapter types output the same kind of table, create one and fill it from the adapter
                $DataTable       = New-Object -TypeName System.Data.DataTable
                $rowCount        = $dataAdapter.fill($dataTable)
                Write-Verbose -Message "Query returned $rowCount row(s)"
            }
            catch {
                Write-Warning "An Error occured trying to run the query, the error was $([Environment]::NewLine + $_.Exception.InnerException))"
            }
        }
      #endregion
      #region send the table to Excel
        #remove parameters which relate to querying SQL, leaving the ones used by Export-Excel
        'Connection' , 'Database'  , 'Session' , 'MsSQLserver' , 'SQL'  , 'DataTable'  , 'QueryTimeout' , 'Force' |
                ForEach-Object {$null = $PSBoundParameters.Remove($_) }
        #if force was specified export even if there are no rows. If there are no columns, the query failed and export "null" if forced
        if     ($DataTable.Rows.Count) {
             Export-Excel  @PSBoundParameters -InputObject $DataTable
        }
        elseif ($Force -and $DataTable.Columns.Count) {
            Write-Warning -Message "Zero rows returned, and -Force was specified, sending empty table to Excel."
            Export-Excel  @PSBoundParameters -InputObject $DataTable
        }
        elseif ($Force) {
            Write-Warning -Message "-Force was specified but there is no data to send."
            Export-Excel  @PSBoundParameters -InputObject $null
        }
        else   {Write-Warning -Message 'There is no Data to insert, and -Force was not specified.' }
      #endregion
      #If we were passed a connection and opened a session, close that session.
        if     ($Connection)  {$Session.close() }
    }
}