kenger.SQLData.psm1


function New-SQLConn {
    <#
    .SYNOPSIS
    Create a new SQL Connection object
    .DESCRIPTION
    Returns a new SQL Connection object, than can hold the connection details of a SQL connection,
    that may be used several times in the same script
   
    .PARAMETER Server
    IP or hostname of the SQL database server you want to connect to.
    .PARAMETER Database
    Database name of the database you want to access.
    .PARAMETER sqlCredentials
    Username and password for the sql server
   
    .OUTPUTS ConnectionObject
    Object that contains connection the information given,
    so that they can be reused
    #>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
    param
    (
        [Parameter(Mandatory=$True,
        HelpMessage='What IP or hostname do you want to connect to?')]
        [string]$server,

        [Parameter(Mandatory=$true,
        HelpMessage='What database do you want to access?')]
        [string]$database,

        [Parameter(
            Mandatory=$false,
            ParameterSetName='Trusted')]
        [switch]$trustedconnection,

        [Parameter(
            Mandatory=$false,
            HelpMessage='SQL Username and Password',
            ParameterSetName='NonTrusted')]
        [PSCredential]$sqlCredentials
    )

    begin {
        if ( $trustedconnection -eq $false -and $sqlCredentials.UserName -eq '' ) {
            throw "Missing sqlCredentials parameter, or use -TrustedConnection switch"
        }
    }

    process {
        $info = @{};
        $info.Server = $server;
        $info.Database = $database;
        $info.sqlCredentials = $sqlCredentials;
        $info.TrustedConnection = $trustedconnection;
        
        if ( $trustedconnection ) {
            $info.ConnectionString = "server='$($info.Server)';database='$($info.Database)';trusted_connection=true;"
        }else{
            $info.ConnectionString = "server='$($info.Server)';database='$($info.Database)';User Id='$($info.sqlCredentials.Username)';Password='$($info.sqlCredentials.GetNetworkCredential().Password)'"
        }

        $conn = New-Object -TypeName PSObject -Prop $info;
        return $conn;
    }
}

function Read-SQLData {
    <#
    .SYNOPSIS
    Read data from SQL database
    .DESCRIPTION
    Returns a datatable that holds the selected data
   
    .EXAMPLE
    Read-SQLData -query "SELECT * FROM table" -Conn $conn
 
    .PARAMETER Query
    SQL query
    .PARAMETER Conn
    Connection Object
    #>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
    param
    (
        [Parameter(Mandatory=$true, HelpMessage='SQL query?')]
        [string]$query,

        [Parameter(
            Mandatory=$true, 
            HelpMessage='Connection object (New-SQLConn)')]
        [PSObject]$conn
    )

    begin {
        
    }

    Process {
        Try {
            Write-Verbose "ConnectionString: $($conn.ConnectionString)";
            
            $Connection = New-Object System.Data.SQLClient.SQLConnection
            $Connection.ConnectionString = $conn.ConnectionString
            $Connection.Open()
            $Command = New-Object System.Data.SQLClient.SQLCommand
            $Command.Connection = $Connection

            $Command.CommandText = $query
            Write-Verbose "Query: $query";
            $result = $Command.ExecuteReader()
    
            $table = new-object "System.Data.DataTable"
            $table.Load($result)

            $Connection.Close()

            Write-Verbose "$($table.Rows.Count) records returned";
            $table;
        }
        Catch {
            Write-Host -BackgroundColor Red "Error: $($_.Exception)"
            Break
        }
    }

    End {
        If ($?) {
            #Write-Host 'Completed Successfully.'
            #Write-Host ' '
        }
    }
}

function Write-SQLData {
    <#
    .SYNOPSIS
    Run SQL query on a MS SQL database
    .DESCRIPTION
    Save or update data to a database, or run a SQL command. This command return the number of rows affected, if $false is returned, something when wrong, try -verbose
   
    .EXAMPLE
    Write-SQLData -query "INSERT * FROM table () VALUES ('aaa','bbb')" -Conn $conn
 
    .PARAMETER Query
    SQL query
    .PARAMETER Conn
    Connection Object
    #>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
    param
    (
        [Parameter(Mandatory=$true, HelpMessage='SQL query?')]
        [string]$query,

        [Parameter(Mandatory=$true, HelpMessage='Connection object')]
        [PSObject]$conn
)

    begin {
        
    }

    process {
        try {
            Write-Verbose "ConnectionString: $($conn.ConnectionString)";
            $Connection = New-Object System.Data.SQLClient.SQLConnection
            $Connection.ConnectionString = $conn.ConnectionString
            $Connection.Open()
            $Command = New-Object System.Data.SQLClient.SQLCommand
            $Command.Connection = $Connection

            Write-Verbose "Query: $query";
            if ( $WhatIfPreference ) {
                Write-Host "Whatif, this query would have been executed: $query" -ForegroundColor Yellow;
            }

            $Command.CommandText = $query

            $rowCount = 0;
            if ($WhatIfPreference -eq $false) {
                $rowCount = $Command.ExecuteNonQuery()
            }

            $Connection.Close()

            Write-Verbose "Rows Updated: $rowCount"
        } catch {
            Write-Host -BackgroundColor Red "Error: $($_.Exception)"
            return $false;
        }
    }

    end {
        If ($?) {
            return $rowCount;
            #Write-Host 'Completed Successfully.'
            #Write-Host ' '
        }
    }

}

function Write-SQLObject {
    <#
    .SYNOPSIS
    Write / Update data to/in a SQL database
    .DESCRIPTION
    Save or update data a table, by providing a object with properties matching to the table columns, this command gives no SQL data as result, only succeded or not
   
    .EXAMPLE
    Write-SQLData -object $obj -Conn $conn
    .EXAMPLE
    Write-SQLData $obj -Conn $conn
 
    .PARAMETER Object
    Data Object (PSObject)
    .PARAMETER table
    Name of the data table to use
    .PARAMETER mode
    Default mode is insert, change if you need to update or delete data.
 
    Insert mode is used for inserting a new row into a table.
 
    Update mode is used for updating an existing row, remember to provide -objectkeys to tell what row that should be updated,
    ex. (-updatekeys ID) will tell that the ID columns is unique.
 
    Delete mode, is used to delete data from a table. The -objectkeys is also required to tell what row that should be deleted.
 
    InsertOrUpdate mode is a dynamic way to wrap the Insert and Update statetent in a TSQL IF statement,
    so it will check if the -updatekeys is found, and update the row, or if not found, insert as a new row.
 
    .PARAMETER objectKeys
    Used if you are using Update or InsertOrUpdate modes, lets the module know what rows you want to update (like a where clause)
    .PARAMETER Conn
    Connection Object
    .PARAMETER UpdateWithEmptyData
    Tells the module that you want to insert or update empty data such as '' into the db, ex. in case of empty properties on your data object.
    .PARAMETER IgnoreColumns
    List of columns to ignore and leave out from insert or updates, ex. auto incremental primary keys -IgnoreColumns id
    #>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
    param
    (
        [Parameter(Mandatory=$true, HelpMessage='Data Object?', ValueFromPipeline=$True)]
        [PSObject]$object,

        [Parameter(Mandatory=$true, HelpMessage='Table name?')]
        [string]$table,

        [Parameter(Mandatory=$false)]
        [ValidateSet('Insert', 'Update', 'Delete', 'InsertOrUpdate')]
        [string]$mode = 'Insert',

        [Parameter(Mandatory=$false)]
        [string[]]$objectKeys = $null,

        [Parameter(Mandatory=$true)]
        [PSObject]$conn,

        [Parameter(Mandatory=$false)]
        [switch]$UpdateWithEmptyData = $false,

        [Parameter(Mandatory=$false)]
        [string]$IgnoreColumns = ''

    )

    begin {
        
        Write-Verbose "Mode: $mode";
        if ( $mode -ne 'Insert' -and $objectKeys -eq $null) {
            throw "You need to define -objectKeys when using Update or InsertOrUpdate modes"
        }

        $rowCount = 0;
    }

    process {
        try {
            $object | % {
                $obj = $_

                #[System.Collections.Generic.List[System.Object]]($columns)
                $columns = [System.Collections.Generic.List[System.Object]]($obj | get-member -MemberType NoteProperty,Properties | select -exp Name);
                $values  = [System.Collections.Generic.List[System.Object]]($obj | get-member -MemberType NoteProperty,Properties | select -exp Name | % { "'$($obj.$_ -Replace "'","''")'" });
            
                # we need the original key/value sets because the columns and values are filtered, emptydata and ignored columns are removed..
                # we still need to use these ignored keys in where clause for updating, ex. by id column... we could create then as a PSObject instead with a ignored flag? (TODO)
                $whereColumns = [System.Collections.Generic.List[System.Object]]($obj | get-member -MemberType NoteProperty,Properties | select -exp Name);
                $whereValues  = [System.Collections.Generic.List[System.Object]]($obj | get-member -MemberType NoteProperty,Properties | select -exp Name | % { "'$($obj.$_ -Replace "'","''")'" });
            
                # handle the UpdateWithEmptyData switch, if not set, dont insert or update with empty data '', remove the column and values from the lists.
                for( $i=0; $i -lt $columns.ToArray().count; $i++) {
                    if ( ( $UpdateWithEmptyData -ne $true -and $values[$i] -eq "''" ) -or ( $IgnoreColumns -contains $columns[$i] ) ) {
                        $columns.RemoveAt($i);
                        $values.RemoveAt($i);
                    }
                }

                # todo ^ handle int or String excape ($obj.ConnectionString.GetType() | select -exp name) == "String"
                # ^ really no need.. int can be handles as strings


                # Generate Update and Where string, for use in Update and InsertOrUpdate modes
                $list = @(); 
                for( $i=0; $i -lt $columns.count; $i++) { 
                    $list += "[$($columns[$i])]=$($values[$i])" 
                } 

                $updateString = $list -join ", ";
                $whereString = ($objectKeys | % { "[$_]=$($whereValues[$whereColumns.IndexOf($_)])"}) -join " AND ";
                
                $columns = $columns | % { "[$_]" }
                
                switch ($mode)
                {
                    'Insert' {
                        $query = "INSERT INTO $table ($( $columns -join ", ")) VALUES ($( $values -join ", "))";
                    }
                    'Update' {
                        $query = "UPDATE $table SET $updateString WHERE ($whereString)"
                    }
                    'InsertOrUpdate' {
                        $query = @"
                                    IF EXISTS (SELECT * FROM $table WHERE $whereString)
                                        BEGIN
                                            UPDATE $table SET $updateString WHERE ($whereString)
                                        END
                                    ELSE
                                        BEGIN
                                            INSERT INTO $table ($( $columns -join ", ")) VALUES ($( $values -join ", "))
                                        END
"@

                    }
                    'Delete' {
                        $query =  "DELETE FROM $table WHERE ($whereString)"
                    }
                }

                $rowCount += Write-SQLData -query $query -conn $conn;
            }
        } catch {
            Write-Host -BackgroundColor Red "Error: $($_.Exception)"
            break;
        }
    }

    end {
        If ($?) {
            $rowCount
            #Write-Host 'Completed Successfully.'
            #Write-Host ' '
        }

        #return $?;
    }

}