OledbSql.psm1

Set-StrictMode -Version 'Latest'

function New-OledbConnection {
<#
.SYNOPSIS
Returns a new System.Data.OleDb.OleDbConnection object.
 
.DESCRIPTION
Returns a new System.Data.OleDb.OleDbConnection object given a connection
string or alias found in a file. Defaults to Module_Root\SQLConnections.txt.
Queries the user for input when a connection string has parameters specified
between percent signs are found, %Example Parameter%.
 
.PARAMETER ConnectionString
Any valid OleDB connection string. Prompts a user for input if a paramter
specified between percent signs is found, %Example Parameter%.
 
.PARAMETER List
List the alias names and their connection string values found in the
connection file.
 
.PARAMETER File
File used for resolving aliases to the full connection string.
Defaults to SQLConnections.txt.
 
.OUTPUTS
System.Data.OleDb.OleDbConnection
 
.NOTES
 
SEE ALSO
    about_OledbSql
 
#>

    [CmdletBinding(
        DefaultParameterSetName="receive"
    )]
    [OutputType([System.Data.OleDb.OleDbConnection])]
    param(
        [Parameter(
            Mandatory=$true,
            ParameterSetName="receive",
            ValueFromPipeline=$true,
            Position=0,
            HelpMessage='Enter an OLEDB connection string'
        )]
        [String]$ConnectionString,

        [Parameter(
            ParameterSetName="give"
        )]
        [Switch]$List,
        [String]$File = (Join-Path $PSScriptRoot SQLConnections.txt)
    )
<#
    Parse optional connection string file with the following format:
      # Comment
      alias OleDB Connection String
#>

    $connectionRegex = "^\s*(?<Alias>[^#]\w+)\s+(?<ConnectionString>.+)$"
    $Connections = @{}
    if (Test-Path $File) {
        get-Content $File | 
            where { $_ -match $connectionRegex } | 
                foreach { $Connections[$Matches['Alias']] = $Matches['ConnectionString'] }
    }    
    if ($PsCmdlet.ParameterSetName -eq 'give' -and $List) { return $Connections }
    
    # Query user to replace any %variables%
    if ($Connections -is [Hashtable] -and $Connections.ContainsKey($ConnectionString)) {
        $ConnectionString = $Connections.Item($ConnectionString) 
    }
    while ($ConnectionString -match "%([^%\s][^%]*)%") {
        $ConnectionString = $ConnectionString -replace $Matches[0],$(Read-Host $Matches[1])
    }

    if (-not $ConnectionString) { 
        throw "No OleDB connection string specified."
    }
    Write-Verbose "Connection string: $ConnectionString"
    new-object System.Data.OleDb.OleDbConnection -ArgumentList $ConnectionString
}


function Invoke-OledbSql {
<#
.SYNOPSIS
Invokes SQL commands through an OleDB Connection.
 
.DESCRIPTION
When a Select is invoked, returns System.Management.Automation.PSCustomObject
objects. When an Update, Insert or Delete is invoked, it returns the affected
count.
 
.PARAMETER SQL
SQL command to be invoked.
 
.PARAMETER Connection
Any valid OleDB connection string or a System.Data.OleDb.OleDbConnection object.
Strings are passed to New-OledbConnection to be resolved.
Defaults to using the variable $Conn if set to a SQLConnection object.
 
.PARAMETER Timeout
Sets the command timeout in seconds.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
A custom type can be set with TypeName parameter.
 
.EXAMPLE
Invoke-OledbSql 'select 1 as Ping' 'Provider=sqloledb; Data Source=ServerName; Initial Catalog=DatabaseName;Integrated Security=SSPI;'
 
Tests SQL connectivity.
 
.EXAMPLE
$cs = 'Provider=sqloledb; Data Source=ServerName; Initial Catalog=DatabaseName;Integrated Security=SSPI;'
$c = New-OledbConnection $cs
Get-Content 'Update_statements_one_per_line.txt' | WHERE { (Invoke-OledbSql -Sql $_ -Connection $c -KeepOpen) -eq 0 } | Set-Content 'failed.txt' -PassThru
$c.Close()
 
 
.NOTES
Though this will handle more than one SQL statement in a single call, it is not recommended.
 
SEE ALSO
    about_OledbSql
 
#>

    [CmdletBinding()]
    [OutputType([PsCustomObject])]
    param(
        [Parameter(Mandatory=$True, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true,
            HelpMessage='Enter SQL command'
        )]
        [string[]]$Sql,

        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true,
            HelpMessage='Enter a type name for the object returned
        '
)]
        [string[]]$TypeName,

        [Parameter(HelpMessage='Enter an OLEDB connection string')]
        $Connection,

        [int]$Timeout,
        
        [switch]$KeepOpen
    )


    begin {
        # Will use the variable $Conn if it is set in the current context.
        if ((-not $Connection) -and ($Conn -is [System.Data.OleDb.OleDbConnection])) {$Connection = $Conn}

        if ($Connection -isnot [System.Data.OleDb.OleDbConnection]) {
            $Connection = New-OledbConnection -Connection:$Connection
        }

        if ($Connection -isnot [System.Data.OleDb.OleDbConnection]) {
            throw "No OleDB connection or string provided."
        }
    }


    process {
        # Handles an array or piped list of sql statements.
        Foreach ($s in $SQL) {
            $command = New-Object System.Data.OleDb.OleDbCommand $s, $Connection
            if ($Timeout) {$command.CommandTimeOut = $Timeout}
            try {
                # If the connection started open, keep it open.
                if ($Connection.State -eq 'Open') {
                    $KeepOpen = $true
                } else {
                    $Connection.Open()
                }

                Write-Verbose ('SQL: {0}' -f $s)
                [System.Data.OleDB.OleDbDataReader]$reader = $command.ExecuteReader()

                # while $reader.NextResult(), handles multiple queries/results in a single SQL request.
                do {
                    # For scalar values, return the records affected.
                    if ( -not ($reader.HasRows)) { return $reader.RecordsAffected }

                    # Create an object template, with the uniqueified column names
                    # as properties and a custom PSTypename if specified.
                    #TODO: Look at maybe using $reader.GetSchemaTable()
                    $columns = 0 .. ($reader.VisibleFieldCount -1) | Foreach {$reader.GetName($_)}
                    $columns = @(Uniqueify $columns)
                    $columnHash = [ordered]@{}
                    if ($TypeName) { $columnHash['Pstypename'] = $TypeName }
                    $columns | Foreach { $columnHash[$_] = $null }
                    $objectTemplate = [pscustomobject]$columnHash

                    while ($reader.Read()) {
                        $o = $objectTemplate.PsObject.Copy()
                        for ($i=0; $i -lt $columns.Count; $i++) {
                            $column = $columns[$i]
                            Write-Debug ('{0} [{1}] ({2}) = [{3}]' -f $i, $column, $reader.GetFieldType($i), $reader.GetValue($i))
                            $o.$column = if ($reader.IsDBNull($i)) { $null } else { $reader.GetValue($i) }
                        }
                        Write-Output $o
                    }
                } while ( $reader.NextResult() )

                if ($reader -and (-not $reader.IsClosed)) {$reader.Close()}
            }
            catch {
                Write-Verbose ('Message: {0}' -f $_.exception.InnerException.message)
                throw $_
            }
            finally {
                $reader = $null
                $command = $null
                if (-not $KeepOpen) {
                    $Connection.Close()
                    $Connection = $null
                }
            }
        }
    }
}


function Uniqueify ([string[]]$Columns) {
<#
.SYNOPSIS
    Helper funciton that appends incrementing numbers to any duplicate
    values in an incoming array.
#>

    $rtn = @()
    for ($i = 0; $i -lt $Columns.Length; $i++) {
        $cnt = 0
        for ($j = 0; $j -lt $Columns.Length -and $j -lt $i; $j++) {
            if ($Columns[$i] -eq $Columns[$j]) { $cnt++ }
        }
        if ($cnt -gt 0) {
            $rtn += ('{0}{1}' -f $Columns[$i], $cnt)
        } else {
            $rtn += $Columns[$i]
        }
    }
    return $rtn
}