powershellimportexcel

Pipelining a System.Data.IDataReader to the Export-Excel Function of the ImportExcel Powershell Module


I have a PS script (called by a SQL job) that queries a SQL DB and writes the result to an Excel file using the Export-Excel function of the PS module ImportExcel. It works great for mid-sized datasets, but it is very slow for larger ones. I suspect this has a lot to do with having to first load query results into a System.Data.DataTable, so I modified Export-Excel to accept a System.Data.IDataReader. This was a huge performance improvement, but I can only get it to work when I explicitly pass a System.Data.IDataReader using the InputObject parameter of the Export-Excel function. Whenever I attempt to pass it via the pipeline, it writes an empty Excel file. To make sure that the pipeline section of the code was working correctly, I moved it to the begin block of the function that runs when an InputObject is passed in explicitly, and it worked without issue.

This works:

Export-Excel -InputObject $reader -Path $filePath -NoNumberConversion * -FreezeTopRow -AutoFilter -MoveToStart

This Fails:

$reader | Export-Excel -Path $filePath -NoNumberConversion * -FreezeTopRow -AutoFilter -MoveToStart

I would like to include the code for the entire function, but unfortunately, my question would exceed the 30,000 character limit. That said, below is the offending code from within the process block:

if ($null -ne $InputObject -and ($null -ne $InputObject.GetType().GetInterface("IDataReader")) ) {
    # get field names the first time around
    if ($firstTimeThru) {
        $firstTimeThru = $false
        $script:Header = @()
        for ($j = 0; $j -lt $InputObject.FieldCount; $j++) {
            $script:Header += $InputObject.GetName($j)
        }

        # region Write Headers to worksheet
        if ($DisplayPropertySet -and ($null -ne $ExcludeProperty) -and ($ExcludeProperty.Length -gt 0) ) {
            $script:Header = $script:Header | Where-Object { $_ -notin $ExcludeProperty }
        }

        if ( ($null -ne $ExcludeProperty) -and ($ExcludeProperty.Length -gt 0)) {
            foreach ($exclusion in $ExcludeProperty) { 
                $script:Header = $script:Header -notlike $exclusion 
            }
        }

        if ($NoHeader) {
            # Don't push the headers to the spreadsheet
            $row -= 1
        }
        else {
            $ColumnIndex = $StartColumn
            foreach ($Name in $script:Header) {
                $ws.Cells[$row, $ColumnIndex].Value = $Name
                Write-Verbose "Cell '$row`:$ColumnIndex' add header '$Name'"
                $ColumnIndex += 1
            }
        }
        #endregion
    }
                     
    #region Add databody values
    $row++
    $ColumnIndex = $StartColumn
    foreach ($Name in $script:Header) {
        $j = $InputObject.GetOrdinal($Name)

        $fieldType = $InputObject.GetFieldType($j)
        $fieldValue = $null

        try {
            switch ($fieldType) {
                { $_ -is [Int32] } {
                    $fieldValue = $InputObject.GetInt32($j); break
                }
                { $_ -is [String] } {
                    $fieldValue = $InputObject.GetString($j); break
                }
                { $_ -is [Boolean] } {
                    $fieldValue = $InputObject.GetBoolean($j); break
                }
                { $_ -is [DateTime] } {
                    $fieldValue = $InputObject.GetDateTime($j); break
                }
                { $_ -is [TimeSpan] } {
                    $fieldValue = [TimeSpan]$InputObject.GetValue($j); break
                }
                { $_ -is [Decimal] } {
                    $fieldValue = $InputObject.GetDecimal($j); break
                }
                { $_ -is [Double] } {
                    $fieldValue = $InputObject.GetDouble($j); break
                }
                { $_ -is [Boolean] } {
                    $fieldValue = $InputObject.GetBoolean($j); break
                }
                { $_ -is [Float] } {
                    $fieldValue = $InputObject.GetFloat($j); break
                }
                { $_ -is [Single] } {
                    $fieldValue = $InputObject.GetFloat($j); break
                }
                { $_ -is [Int64] } {
                    $fieldValue = $InputObject.GetInt64($j); break
                }
                { $_ -is [Int16] } {
                    $fieldValue = $InputObject.GetInt16($j); break
                }
                { $_ -is [Byte] } {
                    $fieldValue = [int]$InputObject.GetByte($j); break
                }
                { $_ -is [Char] } {
                    $fieldValue = $InputObject.GetChar($j).ToString(); break
                }
                { $_ -is [Guid] } {
                    $fieldValue = $InputObject.GetGuid($j).ToString(); break
                }
                { $_ -is [Object] } {
                    $fieldValue = $InputObject.GetValue($j); break
                }
                default {
                    throw "Unsupported field type: $($fieldType.FullName)"
                }
            }

            if ($InputObject.IsDBNull($j)) { $fieldValue = $null }

            if ($fieldType -is [DateTime]) {
                $ws.Cells[$row, $ColumnIndex].Value = $fieldValue
                $ws.Cells[$row, $ColumnIndex].Style.Numberformat.Format = 'm/d/yy h:mm' # This is not a custom format, but a preset recognized as date and localized.
            }
            elseif ($fieldType -is [TimeSpan]) {
                $ws.Cells[$row, $ColumnIndex].Value = $fieldValue
                $ws.Cells[$row, $ColumnIndex].Style.Numberformat.Format = '[h]:mm:ss'
            }
            elseif ($fieldType -is [System.ValueType]) {
                $ws.Cells[$row, $ColumnIndex].Value = $fieldValue
                if ($setNumformat) { $ws.Cells[$row, $ColumnIndex].Style.Numberformat.Format = $Numberformat }
            }
            elseif ($fieldType -isnot [String] -or $null -eq $fieldValue ) {
                #Other objects or null.
                if ($null -ne $fieldValue ) { $ws.Cells[$row, $ColumnIndex].Value = $fieldValue.ToString() }
            }
            elseif ($fieldValue[0] -eq '=') {
                $ws.Cells[$row, $ColumnIndex].Formula = ($fieldValue -replace '^=', '')
                if ($setNumformat) { $ws.Cells[$row, $ColumnIndex].Style.Numberformat.Format = $Numberformat }
            }
            else {
                if ( $NoHyperLinkConversion -ne '*' -and # Put the check for 'NoHyperLinkConversion is null' first to skip checking for wellformedstring
                        $NoHyperLinkConversion -notcontains $Name -and
                        [System.Uri]::IsWellFormedUriString($fieldValue, [System.UriKind]::Absolute)
                    ) { 
                    if ($fieldValue -match "^xl://internal/") {
                        $referenceAddress = $fieldValue -replace "^xl://internal/" , ""
                        $display = $referenceAddress -replace "!A1$"   , ""
                        $h = New-Object -TypeName OfficeOpenXml.ExcelHyperLink -ArgumentList $referenceAddress , $display
                        $ws.Cells[$row, $ColumnIndex].HyperLink = $h
                    }
                    else { $ws.Cells[$row, $ColumnIndex].HyperLink = $fieldValue }
                    $ws.Cells[$row, $ColumnIndex].Style.Font.Color.SetColor([System.Drawing.Color]::Blue)
                    $ws.Cells[$row, $ColumnIndex].Style.Font.UnderLine = $true
                }
                else {
                    $number = $null
                    if ( $NoNumberConversion -ne '*' -and # Check if NoNumberConversion isn't specified. Put this first as it's going to stop the if clause. Quicker than putting regex check first
                        $numberRegex.IsMatch($fieldValue) -and # and if it contains digit(s) - this syntax is quicker than -match for many items and cuts out slow checks for non numbers
                        $NoNumberConversion -notcontains $Name -and
                        [Double]::TryParse($fieldValue, [System.Globalization.NumberStyles]::Any, [System.Globalization.NumberFormatInfo]::CurrentInfo, [Ref]$number)
                    ) {
                        $ws.Cells[$row, $ColumnIndex].Value = $number
                        if ($setNumformat) { $ws.Cells[$row, $ColumnIndex].Style.Numberformat.Format = $Numberformat }
                    }
                    else {
                        $ws.Cells[$row, $ColumnIndex].Value = $fieldValue
                    }
                }
            }
        }
        catch { Write-Warning -Message "Could not insert the '$Name' property at Row $row, Column $ColumnIndex" }

        $ColumnIndex += 1
    }
    #endregion
}

What am I missing here?


Solution

  • As mentioned in the comments, when PowerShell's pipeline processor enumerates an IDataReader, the enumerated contents are all of type IDataRecord, not IDataReader.

    Change the condition to test for IDataRecord and then change behavior based on whether the input object is also an IDataReader:

    if ($PSBoundParameters.ContainsKey('InputObject') -and $InputObject -is [System.Data.IDataRecord]) {
      if ($firstTimeThru) {
        $firstTimeThru = $false
        
        # do the header parsing and preparation like before
    
        if ($InputObject -is [System.Data.IDataReader]) {
          # special case - someone passed a reader by name, we need to manually enumerate
          # place a `while($InputObject.Read()){...}` here, or recurse
        }
      }
    }
    

    You'll probably want to split some of the subsequent code into separate functions so you can reuse it regardless of whether you're processing 1 record or a whole set at a time


    I should add that your type-switch in the middle of the sample code you've provided probably isn't going to work as you expect - the value returned by IDataRecord.GetFieldType() is already of type [type], so the -is operator won't work here - you need to either use -eq for an explicit comparison, or wrap the target type literals used as case labels in $(...) to prevent them being parsed as string literals:

    $fieldValue = switch ($fieldType) { 
      $([int]) { $InputObject.GetInt32($j); break }
      # or with `-eq`
      { $_ -eq [string] } { $InputObject.GetString($j); break }
    }